分类

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

技术文章列表

  • 基于python构建搜索引擎系列——(一)简介 精华

    我们上网用得最多的一项服务应该是搜索,不管大事小情,都喜欢百度一下或谷歌一下,那么百度和谷歌是怎样从浩瀚的网络世界中快速找到你想要的信息呢,这就是搜索引擎的艺术,属于信息检索的范畴。
    这学期学习了《现代信息检索》课程,使用的是Stanford的教材Introduction to Information Retrieval,网上有电子版,大家可以参考。
    本课程的大作业是完成一个新闻搜索引擎,要求是这样的:定向采集3-4个新闻网站,实现这些网站信息的抽取、索引和检索。网页数目不少于10万条。能按相关度、时间和热度(需要自己定义)进行排序,能实现相似新闻的自动聚类。
    截止日期12月31日,我们已经在规定时间完成了该系统,自认为检索效果不错,所以在此把过程记录如下,欢迎讨论。
    网上有很多开源的全文搜索引擎,比如Lucene、Sphinx、Whoosh等,都提供了很好的API,开发者只需要调用相关接口就可以实现一个全功能的搜索引擎。不过既然学习了IR这门课,自然要把相关技术实践一下,所以我们打算自己实现一个搜索引擎。
    这是简介部分,主要介绍整个搜索引擎的思路和框架。

    上图为本搜索引擎的框架图。首先爬虫程序从特定的几个新闻网站抓取新闻数据,然后过滤网页中的图片、视频、广告等无关元素,抽取新闻的主体内容,得到结构化的xml数据。然后一方面使用内存式单遍扫描索引构建方法(SPIMI)构建倒排索引,供检索模型使用;另一方面根据向量空间模型计算两两新闻之间的余弦相似度,供推荐模块使用。最后利用概率检索模型中的BM25公式计算给定关键词下的文档相关性评分,BM25打分结合时间因素得到热度评分,根据评分给出排序结果。
    在后续博文中,我会详细介绍每个部分的实现。
    使用方法
    安装python 3.4+环境
    安装lxml html解析器,命令为pip install lxml
    安装jieba分词组件,命令为pip install jieba
    安装Flask Web框架,命令为pip install Flask
    进入web文件夹,运行main.py文件
    打开浏览器,访问 http://127.0.0.1:5000 输入关键词开始测试

    如果想抓取最新新闻数据并构建索引,一键运行./code/setup.py,再按上面的方法测试。
    本文转载自:http://bitjoy.net/2016/01/04/introduction-to-building-a-search-engine-1
    1 留言 2019-05-26 11:34:24 奖励12点积分
  • 高德底图 根据行政区域名 加载边界到地图中(JS)

    高德底图 根据行政区域名 加载边界到地图中(JS)
    代码:
    function map_map1(place){ //初始化地图对象,加载地图 var map = new AMap.Map("map", { resizeEnable: true, center: [117.000923, 36.675807], zoom: 6 }); var district = null; var polygons=[]; //加载行政区划插件 if(!district){ //实例化DistrictSearch var opts = { subdistrict: 0, //获取边界不需要返回下级行政区 extensions: 'all', //返回行政区边界坐标组等具体信息 level: 'district' //查询行政级别为 市 }; } district = new AMap.DistrictSearch(opts); //行政区查询 district.setLevel('district'); district.search(place, function(status, result) { map.remove(polygons)//清除上次结果 polygons = []; var bounds = result.districtList[0].boundaries; if (bounds) { for (var i = 0, l = bounds.length; i < l; i++) { //生成行政区划polygon var polygon = new AMap.Polygon({ strokeWeight: 1, path: bounds[i], fillOpacity: 0.4, fillColor: '#80d8ff', strokeColor: '#0091ea' }); polygons.push(polygon); } } map.add(polygons) map.setFitView(polygons);//视口自适应 });};
    0 留言 2019-05-22 22:57:53 奖励3点积分
  • 枚举并删除系统上Minifilter回调

    背景我们学习内核 Rootkit 编程,那么肯定会接触到各种无 HOOK 回调函数的设置,这些回调函数都是官方为我们做好的接口,我们直接调用就好。这些回调使用方便,运行在底层,功能强大,而且非常稳定。很多杀软、游戏保护等就是设置这些回调,实现对计算机的监控的。
    既然可以设置回调,自然也可以删除回调。如果是自己程序设置的回调,当然可以很容易删除。但是,我们要做的是要枚举系统上存在的回调,不管是不是自己程序创建的,然后,并对这些回调进行删除,使其失效。
    本文要介绍的是枚举并删除系统上 Minifilter 回调,支持 32 位和 64 位、Win7 到 Win10 全平台系统。现在,我把实现的过程和原理整理成文档,分享给大家。
    函数介绍FltEnumerateFilters 函数
    列举系统中所有注册的 Minifilter 驱动程序。
    函数声明
    NTSTATUS FltEnumerateFilters( _Out_ PFLT_FILTER *FilterList, _In_ ULONG FilterListSize, _Out_ PULONG NumberFiltersReturned);
    参数

    FilterList [out]指向调用者分配的缓冲区的指针,该缓冲区接收不透明的过滤器指针数组。此参数是可选的,如果FilterListSize参数的值为零,则该参数可以为NULL。如果FilterListSize在输入上为零,并且FilterList为NULL,则NumberFiltersReturned参数将接收找到的 Minifilter 驱动程序的数量。FilterListSize [in]FilterList参数指向的缓冲区可以容纳的不透明过滤器指针数。该参数是可选的,可以为零。如果FilterListSize在输入上为零,并且FilterList为NULL,则NumberFiltersReturned参数将接收找到的 Minifilter 驱动程序的数量。NumberFiltersReturned [out]指向调用者分配的变量,该变量接收FilterList参数指向的数组中返回的不透明过滤器指针数。如果FilterListSize参数值太小,并且FilterList在输入上不为NULL,FltEnumerateFilters将返回STATUS_BUFFER_TOO_SMALL,并将NumberFiltersReturn设置为指向找到的minifilter驱动程序的数量。此参数是必需的,不能为NULL。
    返回值

    成功,则返回 STATUS_SUCCESS;失败,则返回其它 NTSTATUS 错误码。

    实现原理枚举 Minifilter 驱动程序的回调,并不像枚举进程回调、线程回调、模块加载回调、注册表回调、对象回调那样,需要我们自己逆向寻找数组或是链表的地址,因为,Minifilter 驱动程序提供了 FltEnumerateFilters 内核函数给我们,用来获取系统上所有注册成功的 Minifilter 回调。
    FltEnumerateFilters 函数可以获取系统上所有注册成功的 Minifilter 的过滤器对象指针数组 PFLT_FILTER *。PFLT_FILTER 数据类型在不同的系统上,它的定义是不同的。下面是我们使用 WinDbg 获取 Win10 x64 上的结构定义:
    lkd> dt fltmgr!_FLT_FILTER +0x000 Base : _FLT_OBJECT +0x030 Frame : Ptr64 _FLTP_FRAME +0x038 Name : _UNICODE_STRING +0x048 DefaultAltitude : _UNICODE_STRING +0x058 Flags : _FLT_FILTER_FLAGS +0x060 DriverObject : Ptr64 _DRIVER_OBJECT +0x068 InstanceList : _FLT_RESOURCE_LIST_HEAD +0x0e8 VerifierExtension : Ptr64 _FLT_VERIFIER_EXTENSION +0x0f0 VerifiedFiltersLink : _LIST_ENTRY +0x100 FilterUnload : Ptr64 long +0x108 InstanceSetup : Ptr64 long +0x110 InstanceQueryTeardown : Ptr64 long +0x118 InstanceTeardownStart : Ptr64 void +0x120 InstanceTeardownComplete : Ptr64 void +0x128 SupportedContextsListHead : Ptr64 _ALLOCATE_CONTEXT_HEADER +0x130 SupportedContexts : [7] Ptr64 _ALLOCATE_CONTEXT_HEADER +0x168 PreVolumeMount : Ptr64 _FLT_PREOP_CALLBACK_STATUS +0x170 PostVolumeMount : Ptr64 _FLT_POSTOP_CALLBACK_STATUS +0x178 GenerateFileName : Ptr64 long +0x180 NormalizeNameComponent : Ptr64 long +0x188 NormalizeNameComponentEx : Ptr64 long +0x190 NormalizeContextCleanup : Ptr64 void +0x198 KtmNotification : Ptr64 long +0x1a0 SectionNotification : Ptr64 long +0x1a8 Operations : Ptr64 _FLT_OPERATION_REGISTRATION +0x1b0 OldDriverUnload : Ptr64 void +0x1b8 ActiveOpens : _FLT_MUTEX_LIST_HEAD +0x208 ConnectionList : _FLT_MUTEX_LIST_HEAD +0x258 PortList : _FLT_MUTEX_LIST_HEAD +0x2a8 PortLock : _EX_PUSH_LOCK
    其中,成员 Operations 就存储着 Minifilter 过滤器对象对应的回调信息,数据类型是 FLT_OPERATION_REGISTRATION,该结构是固定的。在头文件 fltKernel.h 里有 FLT_OPERATION_REGISTRATION 结构体定义:
    typedef struct _FLT_OPERATION_REGISTRATION { UCHAR MajorFunction; FLT_OPERATION_REGISTRATION_FLAGS Flags; PFLT_PRE_OPERATION_CALLBACK PreOperation; PFLT_POST_OPERATION_CALLBACK PostOperation; PVOID Reserved1;} FLT_OPERATION_REGISTRATION, *PFLT_OPERATION_REGISTRATION;
    从结构体里面可知,从中可获取 Minifilter 驱动程序的消息类型 MajorFunction,操作前回调函数地址 PreOperation,操作后回调函数地址 PostOperation 等信息。
    所以,遍历系统上所有的 Minifilter 回调,原理就是:

    调用 FltEnumerateFilters 内核函数获取系统上注册成功的 Minifilter 驱动程序的过滤器对象指针数组 PFLT_FILTER *。然后,我们遍历过滤器对象指针 PFLT_FILTER,从中可以获取 Operations 成员的数据,数据类型为 FLT_OPERATION_REGISTRATION,可以从中获取 Minifilter 回调信息。
    要注意的是,由于不同的系统,FLT_FILTER 数据结构的定义都不相同,所以成员 Operations 在数据结构中的偏移也是不固定的。下面是我使用 WinDbg 逆向各个系统中 FLT_FILTER 的数据结构定义,总结出来的 Operations 偏移大小:




    Win 7
    Win 8.1
    Win 10




    32 位
    0xCC
    0xD4
    0xE4


    64 位
    0x188
    0x198
    0x1A8



    删除回调我们可以通过上述介绍的方法,枚举系统中的回调函数。其中,我们不能调用 FltUnregisterFilter 函数删除 Minifilter 回调,因为微软规定 FltUnregisterFilter 函数只能在 Minifilter 自身的驱动程序中调用,不能在其它的驱动程序中调用使用。所以,要删除回调函数可以有 2 种方式。

    直接修改 FLT_OPERATION_REGISTRATION 数据结构中的操作前回调函数和操作后回调函数的地址数据,使其指向我们自己定义的空回调函数地址。这样,当触发回调函数的时候,执行的是我们自己的空回调函数。修改回调函数的前几字节内存数据,写入直接返回指令 RET,不进行任何操作。
    编码实现声明头文件 fltKernel.h:
    #include <fltKernel.h>
    导入库文件 FltMgr.lib:
    右击项目“属性” --> 链接器 --> 输入 --> 在“附加依赖项”中添加 FltMgr.lib。
    遍历 Minifilter 回调// 遍历回调BOOLEAN EnumCallback(){ NTSTATUS status = STATUS_SUCCESS; ULONG ulFilterListSize = 0; PFLT_FILTER *ppFilterList = NULL; ULONG i = 0; LONG lOperationsOffset = 0; PFLT_OPERATION_REGISTRATION pFltOperationRegistration = NULL; // 获取 Minifilter 过滤器Filter 的数量 FltEnumerateFilters(NULL, 0, &ulFilterListSize); // 申请内存 ppFilterList = (PFLT_FILTER *)ExAllocatePool(NonPagedPool, ulFilterListSize *sizeof(PFLT_FILTER)); if (NULL == ppFilterList) { DbgPrint("ExAllocatePool Error!\n"); return FALSE; } // 获取 Minifilter 中所有过滤器Filter 的信息 status = FltEnumerateFilters(ppFilterList, ulFilterListSize, &ulFilterListSize); if (!NT_SUCCESS(status)) { DbgPrint("FltEnumerateFilters Error![0x%X]\n", status); return FALSE; } DbgPrint("ulFilterListSize=%d\n", ulFilterListSize); // 获取 PFLT_FILTER 中 Operations 偏移 lOperationsOffset = GetOperationsOffset(); if (0 == lOperationsOffset) { DbgPrint("GetOperationsOffset Error\n"); return FALSE; } // 开始遍历 Minifilter 中各个过滤器Filter 的信息 __try { for (i = 0; i < ulFilterListSize; i++) { // 获取 PFLT_FILTER 中 Operations 成员地址 pFltOperationRegistration = (PFLT_OPERATION_REGISTRATION)(*(PVOID *)((PUCHAR)ppFilterList[i] + lOperationsOffset)); __try { // 同一过滤器下的回调信息 DbgPrint("-------------------------------------------------------------------------------\n"); while (IRP_MJ_OPERATION_END != pFltOperationRegistration->MajorFunction) { if (IRP_MJ_MAXIMUM_FUNCTION > pFltOperationRegistration->MajorFunction) // MajorFunction ID Is: 0~27 { // 显示 DbgPrint("[Filter=%p]IRP=%d, PreFunc=0x%p, PostFunc=0x%p\n", ppFilterList[i], pFltOperationRegistration->MajorFunction, pFltOperationRegistration->PreOperation, pFltOperationRegistration->PostOperation); } // 获取下一个消息回调信息 pFltOperationRegistration = (PFLT_OPERATION_REGISTRATION)((PUCHAR)pFltOperationRegistration + sizeof(FLT_OPERATION_REGISTRATION)); } DbgPrint("-------------------------------------------------------------------------------\n"); } __except (EXCEPTION_EXECUTE_HANDLER) { DbgPrint("[2_EXCEPTION_EXECUTE_HANDLER]\n"); } } } __except (EXCEPTION_EXECUTE_HANDLER) { DbgPrint("[1_EXCEPTION_EXECUTE_HANDLER]\n"); } // 释放内存 ExFreePool(ppFilterList); ppFilterList = NULL; return TRUE;}
    移除 Minifilter 回调// 移除回调NTSTATUS RemoveCallback(PFLT_FILTER pFilter){ LONG lOperationsOffset = 0; PFLT_OPERATION_REGISTRATION pFltOperationRegistration = NULL; // 开始遍历 过滤器Filter 的信息 // 获取 PFLT_FILTER 中 Operations 成员地址 pFltOperationRegistration = (PFLT_OPERATION_REGISTRATION)(*(PVOID *)((PUCHAR)pFilter + lOperationsOffset)); __try { // 同一过滤器下的回调信息 while (IRP_MJ_OPERATION_END != pFltOperationRegistration->MajorFunction) { if (IRP_MJ_MAXIMUM_FUNCTION > pFltOperationRegistration->MajorFunction) // MajorFunction ID Is: 0~27 { // 替换回调函数 pFltOperationRegistration->PreOperation = New_MiniFilterPreOperation; pFltOperationRegistration->PostOperation = New_MiniFilterPostOperation; // 显示 DbgPrint("[Filter=%p]IRP=%d, PreFunc=0x%p, PostFunc=0x%p\n", pFilter, pFltOperationRegistration->MajorFunction, pFltOperationRegistration->PreOperation, pFltOperationRegistration->PostOperation); } // 获取下一个消息回调信息 pFltOperationRegistration = (PFLT_OPERATION_REGISTRATION)((PUCHAR)pFltOperationRegistration + sizeof(FLT_OPERATION_REGISTRATION)); } } __except (EXCEPTION_EXECUTE_HANDLER) { DbgPrint("[EXCEPTION_EXECUTE_HANDLER]\n"); } return STATUS_SUCCESS;}
    获取 Operations 偏移// 获取 Operations 偏移LONG GetOperationsOffset(){ RTL_OSVERSIONINFOW osInfo = { 0 }; LONG lOperationsOffset = 0; // 获取系统版本信息, 判断系统版本 RtlGetVersion(&osInfo); if (6 == osInfo.dwMajorVersion) { if (1 == osInfo.dwMinorVersion) { // Win7#ifdef _WIN64 // 64 位 // 0x188 lOperationsOffset = 0x188;#else // 32 位 // 0xCC lOperationsOffset = 0xCC;#endif } else if (2 == osInfo.dwMinorVersion) { // Win8#ifdef _WIN64 // 64 位#else // 32 位#endif } else if (3 == osInfo.dwMinorVersion) { // Win8.1#ifdef _WIN64 // 64 位 // 0x198 lOperationsOffset = 0x198;#else // 32 位 // 0xD4 lOperationsOffset = 0xD4;#endif } } else if (10 == osInfo.dwMajorVersion) { // Win10#ifdef _WIN64 // 64 位 // 0x1A8 lOperationsOffset = 0x1A8;#else // 32 位 // 0xE4 lOperationsOffset = 0xE4;#endif } return lOperationsOffset;}
    程序测试在 Win7 32 位系统下,驱动程序正常执行:

    在 Win8.1 32 位系统下,驱动程序正常执行:

    在 Win10 32 位系统下,驱动程序正常执行:

    在 Win7 64 位系统下,驱动程序正常执行:

    在 Win8.1 64 位系统下,驱动程序正常执行:

    在 Win10 64 位系统下,驱动程序正常执行:

    总结我们可以调用 FltEnumerateFilters 来获取系统上所有 Minifilter 驱动程序的过滤器对象,并从中 PFLT_FILTER 经过一定的偏移获取 Operations 成员数据,里面存储着回调信息。其中,不同统统的 FLT_FILTER 定义都不同,所以,Operations 成员的偏移也不相同。大家也不用记忆这些偏移大小,如果需要用到,可以随时使用 WinDbg 来进行逆向查看就好。
    删除回调常用就有 2 种方式,自己根据需要选择一种使用即可。
    参考参考自《Windows黑客编程技术详解》一书
    1 留言 2019-05-20 15:55:10 奖励16点积分
  • 基于ObRegisterCallbacks实现的线程和进程监控及其保护

    背景要实现监控系统线程和进程,并实现对指定线程和进程的保护,在 32 位系统上可以使用 HOOK 技术,HOOK 相关的函数来实现。但是,到了 64 位平台上,就不能继续按常规的 HOOK 方法去实现了。
    好在 Windows 给我们提供了 ObRegisterCallbacks 内核函数来注册系统回调,可以用来注册系统线程回调,监控系统的线程创建、退出等情况,而且还能进行控制;也可以用来注册系统进程回调,可以监控系统的进程创建、退出等情况,而且也能进行控制。这使得我们实现保护指定线程、进程不被结束,提供了可能。
    现在,我就对程序的实现过程和原理进行整理,形成文档,分享给大家。
    函数介绍ObRegisterCallbacks 函数
    注册线程、进程和桌面句柄操作的回调函数。
    函数声明
    NTSTATUS ObRegisterCallbacks( _In_ POB_CALLBACK_REGISTRATION CallBackRegistration, _Out_ PVOID *RegistrationHandle);
    参数

    CallBackRegistration [in]指向指定回调例程列表和其他注册信息的OB_CALLBACK_REGISTRATION结构的指针。RegistrationHandle [out]指向变量的指针,该变量接收一个标识已注册的回调例程集合的值。 调用者将此值传递给ObUnRegisterCallbacks例程以注销该回调集。
    返回值

    成功,则返回 STATUS_SUCCESS,否则,返回其它 NTSTATUS 错误码。
    备注

    驱动程序必须在卸载之前注销所有回调例程。 您可以通过调用ObUnRegisterCallbacks例程来注销回调例程。

    OB_CALLBACK_REGISTRATION 结构体
    typedef struct _OB_CALLBACK_REGISTRATION { USHORT Version; USHORT OperationRegistrationCount; UNICODE_STRING Altitude; PVOID RegistrationContext; OB_OPERATION_REGISTRATION *OperationRegistration;} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;
    成员

    Version请求的对象回调注册版本。 驱动程序应指定OB_FLT_REGISTRATION_VERSION。OperationRegistrationCountOperationRegistration数组中的条目数。Altitude指定驱动程序Altitude的Unicode字符串。一定要存在,不能置为空 NULL,可以任意指定。RegistrationContext当回调例程运行时,系统将RegistrationContext值传递给回调例程。 该值的含义是由驱动程序自定义的。OperationRegistration指向OB_OPERATION_REGISTRATION结构数组的指针。 每个结构指定ObjectPreCallback和ObjectPostCallback回调例程以及调用例程的操作类型。

    OB_OPERATION_REGISTRATION 结构体
    typedef struct _OB_OPERATION_REGISTRATION { POBJECT_TYPE *ObjectType; OB_OPERATION Operations; POB_PRE_OPERATION_CALLBACK PreOperation; POB_POST_OPERATION_CALLBACK PostOperation;} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;
    成员

    ObjectType指向触发回调例程的对象类型的指针。 指定以下值之一:用于进程句柄操作的PsProcessType线程句柄操作的PsThreadType用于桌面句柄操作的ExDesktopObjectType。 此值在Windows 10中受支持,而不在早期版本的操作系统中。Operations指定以下一个或多个标志:OB_OPERATION_HANDLE_CREATE一个新的进程,线程或桌面句柄已被打开或将被打开。OB_OPERATION_HANDLE_DUPLICATE进程,线程或桌面句柄已被或将被复制。PreOperation指向ObjectPreCallback例程的指针。 在请求的操作发生之前,系统调用此例程。PostOperation指向ObjectPostCallback例程的指针。 在请求的操作发生后,系统调用此例程。

    IoThreadToProcess 函数
    返回指向指定线程的进程的指针。
    函数声明
    PEPROCESS IoThreadToProcess( _In_ PETHREAD Thread);
    参数

    Thread[in]要返回进程的指定线程对象。
    返回值

    返回线程对象对应的进程对象的指针。

    PsGetProcessId 函数
    返回与指定进程关联的进程标识符(进程ID)。
    函数声明
    HANDLE PsGetProcessId( _In_ PEPROCESS Process);
    参数

    Process[in]
    指向进程对象结构的指针。

    返回值

    返回进程ID。

    实现原理破解 ObRegisterCallbacks 函数的使用限制第一种方法在讲解怎么使用 ObRegisterCallbacks 函数来注册系统线程、进程回调的之前,先来讲解下 Windows 对这个函数做的限制:驱动程序必须有数字签名才能使用此函数。不过国外的黑客对此限制很不满,通过逆向 ObRegisterCallbacks,找到了破解这个限制的方法。经研究,内核通过 MmVerifyCallbackFunction 验证此回调是否合法, 但此函数只是简单的验证了一下 DriverObject->DriverSection->Flags 的值是不是为 0x20:
    nt!MmVerifyCallbackFunction+0x75: fffff800`01a66865 f6406820 test byte ptr [rax+68h],20h fffff800`01a66869 0f45fd cmovne edi,ebp
    所以破解方法非常简单,只要把 DriverObject->DriverSection->Flags 的值按位或 0x20 即可。其中,DriverSection 是指向 LDR_DATA_TABLE_ENTRY 结构的值,要注意该结构在 32 位和 64 位系统下的定义。
    // 注意32位与64位的对齐大小#ifndef _WIN64 #pragma pack(1) #endiftypedef struct _LDR_DATA_TABLE_ENTRY{ LIST_ENTRY InLoadOrderLinks; LIST_ENTRY InMemoryOrderLinks; LIST_ENTRY InInitializationOrderLinks; PVOID DllBase; PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; USHORT LoadCount; USHORT TlsIndex; union { LIST_ENTRY HashLinks; struct { PVOID SectionPointer; ULONG CheckSum; }; }; union { ULONG TimeDateStamp; PVOID LoadedImports; }; PVOID EntryPointActivationContext; PVOID PatchInformation; LIST_ENTRY ForwarderLinks; LIST_ENTRY ServiceTagLinks; LIST_ENTRY StaticLinks;} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;#ifndef _WIN64 #pragma pack()#endif
    第二种方法使用此函数, 一定要设置 IMAGE_OPTIONAL_HEADER 中的 DllCharacterisitics 字段设置为:IMAGE_DLLCHARACTERISITICS_FORCE_INTEGRITY 属性,该属性是一个驱动强制签名属性。使用 VS2013 开发环境设置方式是:

    右击项目,选择属性;选中配置属性中的链接器,点击命令行;在其它选项中输入: /INTEGRITYCHECK 表示设置; /INTEGRITYCHECK:NO 表示不设置。
    这样,设置之后,驱动程序必须要进行驱动签名才可正常运行!
    调用 ObRegisterCallbacks 注册线程回调以及进程回调我们从上面的函数介绍中,可以知道大概的实现流程:

    首先,在调用 ObRegisterCallbacks 函数注册系统回调之前,我们要先对结构体 OB_CALLBACK_REGISTRATION 进行初始化。设置回调的版本 Version;设置回调的 Altitude,任意指定;设置回调函数的数量 OperationRegistrationCount;设置回调函数 OperationRegistration。其中,OperationRegistration 是一个 OB_OPERATION_REGISTRATION 结构体数组,里面存储着回调对象的类型、操作类型以及回调函数,它的数量要和 OperationRegistrationCount 对应。然后,再调用 ObRegisterCallbacks 进行注册,并保留系统回调对象句柄。最后,在不使用回调的时候,调用 ObUnRegisterCallbacks 函数传入系统回调对象句柄,删除回调。
    其中,线程回调和进程回调的注册,只有 OB_OPERATION_REGISTRATION 结构体的成员 ObjectType 不同。对于线程,ObjectType 为 PsThreadType;对于进程,ObjectType 为 PsProcessType。其它的操作及其含义,均相同。
    线程、进程回调函数中实现线程、进程保护由于在注册系统回调的时候,我们设置监控线程以及进程的操作类型为:OB_OPERATION_HANDLE_CREATE 和 OB_OPERATION_HANDLE_DUPLICATE。要想实现,拒绝结束线程或者进程的操作,我们只需从操作类型句柄信息中去掉相应的结束线程或者进程的权限即可:
    // OB_OPERATION_HANDLE_CREATE 操作类型pObPreOperationInfo->Parameters->CreateHandleInformation.DesiredAccess = 0;
    // OB_OPERATION_HANDLE_DUPLICATE 操作类型pObPreOperationInfo->Parameters->DuplicateHandleInformation.DesiredAccess = 0;
    那么,我们怎么从线程对象或者进程对象 pObPreOperationInfo->Object 中判断是否是保护线程或者进程呢。对于进程,我们可以调用 PsGetProcessImageFileName 函数,从进程结构对象获取进程名称进行判断。对于线程,我们可以通过 IoThreadToProcess 函数,根据线程结构对象获取相应的进程结构对象,再根据 PsGetProcessImageFileName 函数,从进程结构对象获取进程名称进行判断。
    编码实现破解 ObRegisterCallbacks 的使用限制// 编程方式绕过签名检查BOOLEAN BypassCheckSign(PDRIVER_OBJECT pDriverObject){#ifdef _WIN64 typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY listEntry; ULONG64 __Undefined1; ULONG64 __Undefined2; ULONG64 __Undefined3; ULONG64 NonPagedDebugInfo; ULONG64 DllBase; ULONG64 EntryPoint; ULONG SizeOfImage; UNICODE_STRING path; UNICODE_STRING name; ULONG Flags; USHORT LoadCount; USHORT __Undefined5; ULONG64 __Undefined6; ULONG CheckSum; ULONG __padding1; ULONG TimeDateStamp; ULONG __padding2; } KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;#else typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY listEntry; ULONG unknown1; ULONG unknown2; ULONG unknown3; ULONG unknown4; ULONG unknown5; ULONG unknown6; ULONG unknown7; UNICODE_STRING path; UNICODE_STRING name; ULONG Flags; } KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;#endif PKLDR_DATA_TABLE_ENTRY pLdrData = (PKLDR_DATA_TABLE_ENTRY)pDriverObject->DriverSection; pLdrData->Flags = pLdrData->Flags | 0x20; return TRUE;}
    注册线程回调// 设置线程回调函数NTSTATUS SetThreadCallbacks(){ NTSTATUS status = STATUS_SUCCESS; OB_CALLBACK_REGISTRATION obCallbackReg = { 0 }; OB_OPERATION_REGISTRATION obOperationReg = { 0 }; RtlZeroMemory(&obCallbackReg, sizeof(OB_CALLBACK_REGISTRATION)); RtlZeroMemory(&obOperationReg, sizeof(OB_OPERATION_REGISTRATION)); // 设置 OB_CALLBACK_REGISTRATION obCallbackReg.Version = ObGetFilterVersion(); obCallbackReg.OperationRegistrationCount = 1; obCallbackReg.RegistrationContext = NULL; RtlInitUnicodeString(&obCallbackReg.Altitude, L"321001"); obCallbackReg.OperationRegistration = &obOperationReg; // 设置 OB_OPERATION_REGISTRATION // Thread 和 Process 的区别所在 obOperationReg.ObjectType = PsThreadType; obOperationReg.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; // Thread 和 Process 的区别所在 obOperationReg.PreOperation = (POB_PRE_OPERATION_CALLBACK)(&ThreadPreCall); // 注册回调函数 status = ObRegisterCallbacks(&obCallbackReg, &g_obThreadHandle); if (!NT_SUCCESS(status)) { DbgPrint("ObRegisterCallbacks Error[0x%X]\n", status); return status; } return status;}
    注册进程回调// 设置进程回调函数NTSTATUS SetProcessCallbacks(){ NTSTATUS status = STATUS_SUCCESS; OB_CALLBACK_REGISTRATION obCallbackReg = { 0 }; OB_OPERATION_REGISTRATION obOperationReg = { 0 }; RtlZeroMemory(&obCallbackReg, sizeof(OB_CALLBACK_REGISTRATION)); RtlZeroMemory(&obOperationReg, sizeof(OB_OPERATION_REGISTRATION)); // 设置 OB_CALLBACK_REGISTRATION obCallbackReg.Version = ObGetFilterVersion(); obCallbackReg.OperationRegistrationCount = 1; obCallbackReg.RegistrationContext = NULL; RtlInitUnicodeString(&obCallbackReg.Altitude, L"321000"); obCallbackReg.OperationRegistration = &obOperationReg; // 设置 OB_OPERATION_REGISTRATION // Thread 和 Process 的区别所在 obOperationReg.ObjectType = PsProcessType; obOperationReg.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; // Thread 和 Process 的区别所在 obOperationReg.PreOperation = (POB_PRE_OPERATION_CALLBACK)(&ProcessPreCall); // 注册回调函数 status = ObRegisterCallbacks(&obCallbackReg, &g_obProcessHandle); if (!NT_SUCCESS(status)) { DbgPrint("ObRegisterCallbacks Error[0x%X]\n", status); return status; } return status;}
    线程回调函数// 线程回调函数OB_PREOP_CALLBACK_STATUS ThreadPreCall(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION pObPreOperationInfo){ PEPROCESS pEProcess = NULL; // 判断对象类型 if (*PsThreadType != pObPreOperationInfo->ObjectType) { return OB_PREOP_SUCCESS; } // 获取线程对应的进程 PEPROCESS pEProcess = IoThreadToProcess((PETHREAD)pObPreOperationInfo->Object); // 判断是否市保护PID, 若是, 则拒绝结束线程 if (IsProtectProcess(pEProcess)) { // 操作类型: 创建句柄 if (OB_OPERATION_HANDLE_CREATE == pObPreOperationInfo->Operation) { if (1 == (1 & pObPreOperationInfo->Parameters->CreateHandleInformation.OriginalDesiredAccess)) { pObPreOperationInfo->Parameters->CreateHandleInformation.DesiredAccess = 0; } } // 操作类型: 复制句柄 else if (OB_OPERATION_HANDLE_DUPLICATE == pObPreOperationInfo->Operation) { if (1 == (1 & pObPreOperationInfo->Parameters->DuplicateHandleInformation.OriginalDesiredAccess)) { pObPreOperationInfo->Parameters->DuplicateHandleInformation.DesiredAccess = 0; } } } return OB_PREOP_SUCCESS;}
    进程回调函数// 进程回调函数OB_PREOP_CALLBACK_STATUS ProcessPreCall(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION pObPreOperationInfo){ PEPROCESS pEProcess = NULL; // 判断对象类型 if (*PsProcessType != pObPreOperationInfo->ObjectType) { return OB_PREOP_SUCCESS; } // 获取进程结构对象 pEProcess = (PEPROCESS)pObPreOperationInfo->Object; // 判断是否市保护PID, 若是, 则拒绝结束进程 if (IsProtectProcess(pEProcess)) { // 操作类型: 创建句柄 if (OB_OPERATION_HANDLE_CREATE == pObPreOperationInfo->Operation) { if (1 == (1 & pObPreOperationInfo->Parameters->CreateHandleInformation.OriginalDesiredAccess)) { pObPreOperationInfo->Parameters->CreateHandleInformation.DesiredAccess = 0; } } // 操作类型: 复制句柄 else if (OB_OPERATION_HANDLE_DUPLICATE == pObPreOperationInfo->Operation) { if (1 == (1 & pObPreOperationInfo->Parameters->DuplicateHandleInformation.OriginalDesiredAccess)) { pObPreOperationInfo->Parameters->DuplicateHandleInformation.DesiredAccess = 0; } } } return OB_PREOP_SUCCESS;}
    程序测试在 Win7 32 位系统下,驱动程序正常执行:

    在 Win10 64 位系统下,驱动程序正常执行:

    总结其中要注意两个问题:
    一是,破解 ObRegisterCallbacks 函数的使用限制有两种方式,一种是通过编程来解决;一种是通过 VS 开发环境和数字签名来解决。在使用编程方式解决限制的时候,一定要注意 32 位与 64 位下,LDR_DATA_TABLE_ENTRY 结构体的对齐差别。
    二是,OB_CALLBACK_REGISTRATION 中的 Altitude 成员一定要存在,不能置为空 NULL,可以任意指定。
    参考参考自《Windows黑客编程技术详解》一书
    3 留言 2019-02-19 13:04:00 奖励7点积分
  • 基于MuPDF库实现PDF文件转换成PNG格式图片 精华

    背景之所以会接触MuPDF是因为,有位群友在Q群里提问,如何将PDF保存为.PNG图片格式。我一看到这个问题,就蒙了,因为我没有接触过类似的项目或程序。但是,作为一群之主的我,还是要给初学者一个答复的,所以便去网上搜索了相关信息,才了解到有MuPDF开源库的存在。
    MuPDF是一种轻量级的PDF,XPS和电子书阅读器。由各种平台的软件库,命令行工具和查看器组成。支持许多文档格式,如PDF,XPS,OpenXPS,CBZ,EPUB和FictionBook 2。
    后来,自己就根据网上搜索到的一些资料,实现了基于MuPDF库将PDF指定页转换成PNG格式图片的小程序。现在,我就把程序的实现思路和过程写成文档,分享给大家。
    实现思路对于MuPDF库的源码下载以及编译过程,可以参考本站的《使用VS2013编译MuPDF库》这篇文章。
    其实,在MuPDF库中就提供了这一个功能:将PDF指定页转换成PNG格式图片,所以,我们直接编译MuPDF提供的代码就可以了。
    示例程序代码位于MuPDF库源码的“docs”目录下的“example.c”文件,我们只需使用VS2013创建一个空项目,然后把“example.c”文件导入项目中,接着将“include”目录中的头文件以及编译出来的libmupdf.lib、libmupdf-js-none.lib、libthirdparty.lib导入文件中即可。
    其中,“example.c”中主要的函数就是 render 函数,我们主要是把参数传进render函数,就可以把pdf转换成png图片了。
    对于“example.c”的代码可以不用修改,直接编译运行即可。但是,为了方便演示,我们还是对“example.c”中的 main 函数进行修改。
    编码实现以下是“example.c”文件中 render 函数的源码:
    void render(char *filename, int pagenumber, int zoom, int rotation){ // Create a context to hold the exception stack and various caches. fz_context *ctx = fz_new_context(NULL, NULL, FZ_STORE_UNLIMITED); // Open the PDF, XPS or CBZ document. fz_document *doc = fz_open_document(ctx, filename); // Retrieve the number of pages (not used in this example). int pagecount = fz_count_pages(doc); // Load the page we want. Page numbering starts from zero. fz_page *page = fz_load_page(doc, pagenumber - 1); // Calculate a transform to use when rendering. This transform // contains the scale and rotation. Convert zoom percentage to a // scaling factor. Without scaling the resolution is 72 dpi. fz_matrix transform; fz_rotate(&transform, rotation); fz_pre_scale(&transform, zoom / 100.0f, zoom / 100.0f); // Take the page bounds and transform them by the same matrix that // we will use to render the page. fz_rect bounds; fz_bound_page(doc, page, &bounds); fz_transform_rect(&bounds, &transform); // Create a blank pixmap to hold the result of rendering. The // pixmap bounds used here are the same as the transformed page // bounds, so it will contain the entire page. The page coordinate // space has the origin at the top left corner and the x axis // extends to the right and the y axis extends down. fz_irect bbox; fz_round_rect(&bbox, &bounds); fz_pixmap *pix = fz_new_pixmap_with_bbox(ctx, fz_device_rgb(ctx), &bbox); fz_clear_pixmap_with_value(ctx, pix, 0xff); // A page consists of a series of objects (text, line art, images, // gradients). These objects are passed to a device when the // interpreter runs the page. There are several devices, used for // different purposes: // // draw device -- renders objects to a target pixmap. // // text device -- extracts the text in reading order with styling // information. This text can be used to provide text search. // // list device -- records the graphic objects in a list that can // be played back through another device. This is useful if you // need to run the same page through multiple devices, without // the overhead of parsing the page each time. // Create a draw device with the pixmap as its target. // Run the page with the transform. fz_device *dev = fz_new_draw_device(ctx, pix); fz_run_page(doc, page, dev, &transform, NULL); fz_free_device(dev); // Save the pixmap to a file. fz_write_png(ctx, pix, "out.png", 0); // Clean up. fz_drop_pixmap(ctx, pix); fz_free_page(doc, page); fz_close_document(doc); fz_free_context(ctx);}
    程序测试我们修改 main 函数直接调用上述函数接口, main 函数为:
    int main(int argc, char **argv){ // 文件路径 char filename[MAX_PATH] = "C:\\Users\\DemonGan\\Desktop\\test.pdf"; // 转换的页码数 int pagenumber = 1; // 缩放比例 int zoom = 100; // 旋转角度 int rotation = 0; // 处理 render(filename, pagenumber, zoom, rotation); system("pause"); return 0;}
    直接运行程序,目录下有 out.png 图片生成,打开图片查看内容,发现是 test.pdf 的第一页内容,所以转换成功。
    总结这个程序的实现,自己可以不用写代码就可以完成。因为MuPDF已经把接口都封装好了,而且也有示例程序可以直接调用。如果想要把界面做得更有好些,可以把程序写成界面程序,然后直接调用现在的这个 render 函数接口,进行转换即可。
    3 留言 2018-11-06 22:31:04 奖励15点积分
  • 把修改/更新过的项目重新提交至github上

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

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

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

    最后刷新下就OK了。
    0 留言 2019-05-13 16:34:48 奖励2点积分
  • MUI H5文档笔记 精华

    MUI H5文档笔记MUI H5文档笔记界面初始化H5plus初始化创建子页面打开界面参数传递控制页面load显示关闭界面底部导航切换界面自定义事件页面预加载消息框原生模式ActionSheet下拉刷新上拉加载上拉下拉整合手势遮罩滑动导航选择图片轮播自定义导航Ajax-get请求Ajax-post请求照相机访问相册蜂鸣提示音手机震动弹出菜单设备信息手机信息发送短信拨打电话发送邮件本地存储图片上传地理位置设置IOS状态栏手机通讯录启动页设置PHP后台搭建JSON转换隐藏本页面中滚动条首次启动欢迎页数据库增删改查和接口推送浏览器打开新页面PDF浏览自定义下拉刷新即时聊天双击安卓返回键退出QQ登录界面初始化初始化就是把一切程序设为默认状态,把没准备的准备好。mui框架将很多功能配置都集中在mui.init方法中,要使用某项功能,只需要在mui.init方法中完成对应参数配置即可,目前支持在mui.init方法中配置的功能包括:创建子页面、关闭页面、手势事件配置、预加载、下拉刷新、上拉加载。
    H5plus初始化在我们APP的开发中,如果我们用到了H5+的一些API或者接口,我们需要初始化另外一个函数,类属于 JS 中的window.onload 或者 window.ready
    Mui.plusReady(); 所有涉及到H5+的东西,建议写到这个里面
    mui.plusReady(function(){ var w = plus.webview.currentWebview(); console.log(w); });
    创建子页面为防止APP运行过程中内容滚动出现卡顿的现象,所以部分页面我们采用头部和内容分离的形式进行实现,比如头部导航和底部导航
    mui.init({ subpages:[{ url:your-subpage-url,//子页面HTML地址,支持本地地址和网络地址 id:your-subpage-id,//子页面标志 styles:{ top:subpage-top-position,//子页面顶部位置 bottom:subpage-bottom-position,//子页面底部位置 width:subpage-width,//子页面宽度,默认为100% height:subpage-height,//子页面高度,默认为100% ...... }, extras:{ name:'zxd学院'//子页面通过plus.webview.currentWebview().name能拿到这个值 }//额外扩展参数 }] });
    打开界面//打开新窗口mui.openWindow({ url:'target.html', //需要打开页面的url地址 id:'target', //需要打开页面的url页面id styles:{ top:'0px',//新页面顶部位置 bottom:'0px',//新页面底部位置 // width:newpage-width,//新页面宽度,默认为100% // height:newpage-height,//新页面高度,默认为100% ...... }, extras:{ name:'aries', age:'18',// .....//自定义扩展参数,可以用来处理页面间传值 },show:{ //控制打开页面的类型 autoShow:true,// //页面loaded事件发生后自动显示,默认为true aniShow:'zoom-fade-out',//页面显示动画,默认为”slide-in-right“; 页面出现的方式 左右上下 duration:'1000'//页面动画持续时间,Android平台默认100毫秒,iOS平台默认200毫秒; }, waiting:{ // 控制 弹出转圈框的信息 autoShow:true,//自动显示等待框,默认为true title:'WRITE-BUG...',//等待对话框上显示的提示内容 options:{ width:'300px',//等待框背景区域宽度,默认根据内容自动计算合适宽度 height:'300px',//等待框背景区域高度,默认根据内容自动计算合适高度 ...... } }});
    参数传递mui.plusReady(function(){ var self = plus.webview.currentWebview(); //获得当前页面的对象 var name = self.name; //name 和 age 为传递的参数的键 var age = self.age; console.log(name); console.log(age); // 获得首页 专用的 var index = plus.webview.getLaunchWebview(); // 获得指定页面的对象 注意,要确保你的这个页面是存在的, 就是打开过的 var target = plus.webview.getWebviewById('目标页面的id');});
    控制页面load显示show:{ // openwindow 函数内设置 autoShow:false } // 目标页面//从服务器获取数据 .... 这里是业务逻辑//业务数据获取完毕,并已插入当前页面DOM; //注意:若为ajax请求,则需将如下代码放在处理完ajax响应数据之后;mui.plusReady(function(){ //关闭等待框 plus.nativeUI.closeWaiting(); //显示当前页面 mui.currentWebview.show(); });
    关闭界面
    点击包含.mui-action-back类的控件
    在页面上,向右快速滑动
    Android手机按下back按键

    mui框架封装的页面右滑关闭功能,默认未启用,若要使用右滑关闭功能,需要在mui.init();方法中设置swipeBack参数,如下:
    mui.init({ swipeBack:true //启用右滑关闭功能});
    mui框架默认会监听Android手机的back按键,然后执行页面关闭逻辑; 若不希望mui自动处理back按键,可通过如下方式关闭mui的back按键监听:
    mui.init({ keyEventBind: { backbutton: false //关闭back按键监听 }});
    底部导航切换界面HTML部分:
    <nav class="mui-bar mui-bar-tab"> <a id="defaultTab" class="mui-tab-item mui-active" href="a.html"> <span class="mui-icon mui-icon-videocam"></span> <span class="mui-tab-label">社区</span> </a> <a class="mui-tab-item" href="b.html"> <span class="mui-icon mui-icon-chatboxes"><span style="display: none;" class="mui-badge">1</span></span> <span class="mui-tab-label">群组</span> </a> <a class="mui-tab-item" href="c.html"> <span class="mui-icon mui-icon-home"></span> <span class="mui-tab-label">我的</span> </a></nav>
    JavaScript部分:
    //mui初始化mui.init();var subpages = ['a.html', 'b.html', 'c.html'];var subpage_style = { top:'0px', bottom: '51px'}; var aniShow = {}; //创建子页面,首个选项卡页面显示,其它均隐藏;mui.plusReady(function() { var self = plus.webview.currentWebview(); for (var i = 0; i < subpages.length; i++) { var temp = {}; var sub = plus.webview.create(subpages[i], subpages[i], subpage_style); if (i > 0) { sub.hide(); }else{ temp[subpages[i]] = "true"; mui.extend(aniShow,temp); } self.append(sub); }}); //当前激活选项var activeTab = subpages[0]; //选项卡点击事件mui('.mui-bar-tab').on('tap', 'a', function(e) { var targetTab = this.getAttribute('href'); if (targetTab == activeTab) { return; } //显示目标选项卡 //若为iOS平台或非首次显示,则直接显示 if(mui.os.ios||aniShow[targetTab]){ plus.webview.show(targetTab); }else{ //否则,使用fade-in动画,且保存变量 var temp = {}; temp[targetTab] = "true"; mui.extend(aniShow,temp); plus.webview.show(targetTab,"fade-in",300); } //隐藏当前; plus.webview.hide(activeTab); //更改当前活跃的选项卡 activeTab = targetTab;}); //自定义事件,模拟点击“首页选项卡”document.addEventListener('gohome', function() { var defaultTab = document.getElementById("defaultTab"); //模拟首页点击 mui.trigger(defaultTab, 'tap'); //切换选项卡高亮 var current = document.querySelector(".mui-bar-tab>.mui-tab-item.mui-active"); if (defaultTab !== current) { current.classList.remove('mui-active'); defaultTab.classList.add('mui-active'); }});
    自定义事件监听自定义事件 - 目标页
    window.addEventListener('shijian',function(event){ //通过event.detail可获得传递过来的参数内容 .... var name = event.detail.namel console.log(name); shijian(); })
    触发自定义事件 - 本页
    //首先获得目标页面的对象var targetPage = plus.webview.getWebviewById('目标页面id'); mui.fire(targetPage,'shijian',{ //自定义事件参数 name:'write-bug'});
    页面预加载所谓的预加载技术就是在用户尚未触发页面跳转时,提前创建目标页面,这样当用户跳转时,就可以立即进行页面切换,节省创建新页面的时间,提升app使用体验。mui提供两种方式实现页面预加载。
    方式一:通过mui.init方法中的preloadPages参数进行配置
    mui.init({ // 可同时加载一个或者多个界面 preloadPages:[ //加载一个界面 { url:'a.html', id:'a', styles:{},//窗口参数 extras:{},//自定义扩展参数 subpages:[{},{}]//预加载页面的子页面 },{ // 可加载另外一个界面,不需要可直接删除 url:'b.html', id:'b', styles:{},//窗口参数 extras:{},//自定义扩展参数 subpages:[{},{}]//预加载页面的子页面 } ]});
    方式二:通过mui.preload方法预加载,一次只能预加载一个页面,若需加载多个webview,则需多次调用mui.preload()方法
    mui.plusReady(function(){ var productView = mui.preload({ url: 'list.html', id: 'list', }); console.log(productView); //获得预加载界面的对象});
    消息框警告消息框
    mui.alert('欢迎使用Hello WRITE-BUG','WRITE-BUG',function(){ alert('你刚关闭了警告框');});
    消息提示框
    var btnArray = ['是','否']; mui.confirm('WRITE-BUG技术共享平台 - 一个专注校园计算机技术交流的平台,赞?','Hello WRITE-BUG',btnArray,function(e){ if(e.index==0){ alert('点击了- 是'); //自己的逻辑 }else{ alert('点击了- 否'); }});
    输入对话框
    var btnArray = ['确定','取消']; mui.prompt('请输入你对WRITE-BUG的评语:','内容好','WRITE-BUG',btnArray,function(e){ if(e.index==0){ alert('点击了 - 确认'); var value = e.value; // value 为输入的内容 }else{ alert('点击了 - 取消'); }});
    自动消息对话框
    mui.toast('显示内容');
    日期选择框
    //js里的月份 是从0月开始的,也就是说,js中的0月是我们1月var dDate=new Date(); //默认显示的时间dDate.setFullYear(2015,5,30);var minDate=new Date(); //可选择的最小时间minDate.setFullYear(2010,0,1);var maxDate=new Date(); //课选择的最大的时间maxDate.setFullYear(2016,11,31); plus.nativeUI.pickDate( function(e) { var d=e.date; alert('您选择的日期是:'+d.getFullYear()+"-"+(d.getMonth()+1)+"-"+ d.getDate()); },function(e){ alert('您没有选择日期');},{title:"请选择日期",date:dDate,minDate:minDate,maxDate:maxDate});
    时间选择框
    var dTime=new Date();dTime.setHours(20,0); //设置默认时间plus.nativeUI.pickTime(function(e){ var d=e.date; alert("您选择的时间是:"+d.getHours()+":"+d.getMinutes()); },function (e){ alert('您没有选择时间');},{title:"请选择时间",is24Hour:true,time:dTime});
    原生模式ActionSheetvar btnArray = [{title:"分享到微信"},{title:"分享到新浪微博"},{title:"分享到搜狐微博"}]; //选择按钮 1 2 3plus.nativeUI.actionSheet( { title:"分享到", cancel:"取消", // 0 buttons:btnArray }, function(e){ var index = e.index; // alert(index); switch (index){ case 1: //写自己的逻辑 break; case 2: break; }} );
    下拉刷新为实现下拉刷新功能,大多H5框架都是通过DIV模拟下拉回弹动画,在低端android手机上,DIV动画经常出现卡顿现象(特别是图文列表的情况); 通过双webview解决这个DIV的拖动流畅度问题;拖动时,拖动的不是div,而是一个完整的webview(子webview),回弹动画使用原生动画;在iOS平台,H5的动画已经比较流畅,故依然使用H5方案。两个平台实现虽有差异,但经过封装,可使用一套代码实现下拉刷新。
    第一步: 创建子页面,因为拖动的其实是个子页面的整体
    mui.init({ subpages:[{ url:pullrefresh-subpage-url,//下拉刷新内容页面地址 id:pullrefresh-subpage-id,//内容页面标志 styles:{ top:subpage-top-position,//内容页面顶部位置,需根据实际页面布局计算,若使用标准mui导航,顶部默认为48px; .....//其它参数定义 } }] });
    第二步:内容页面需按照如下DOM结构构建
    <!--下拉刷新容器--> <div id="pullrefresh" class="mui-content mui-scroll-wrapper"> <div class="mui-scroll"> <!--数据列表--> <ul class="mui-table-view mui-table-view-chevron"> <li class="mui-table-view-cell">1</li> </ul> </div> </div>
    第三步:通过mui.init方法中pullRefresh参数配置下拉刷新各项参数
    mui.init({ pullRefresh : { container:"#pullrefresh",//下拉刷新容器标识,querySelector能定位的css选择器均可,比如:id、.class等 down : { contentdown : "下拉可以刷新",//可选,在下拉可刷新状态时,下拉刷新控件上显示的标题内容 contentover : "释放立即刷新",//可选,在释放可刷新状态时,下拉刷新控件上显示的标题内容 contentrefresh : "正在刷新...",//可选,正在刷新状态时,下拉刷新控件上显示的标题内容 callback : fn //必选,刷新函数,根据具体业务来编写,比如通过ajax从服务器获取新数据; } } });
    第四步:设置执行函数
    function fn() { //业务逻辑代码,比如通过ajax从服务器获取新数据; ...... //注意,加载完新数据后,必须执行如下代码,注意:若为ajax请求,则需将如下代码放置在处理完ajax响应数据之后 mui('#pullrefresh').pullRefresh().endPulldownToRefresh(); //这行代码会隐藏掉 正在加载... 容器}
    上拉加载第一步,第二步 和下拉刷新的一样
    第三步:通过mui.init方法中pullRefresh参数配置下拉刷新各项参数
    mui.init({ pullRefresh : { container:"#pullrefresh",//待刷新区域标识,querySelector能定位的css选择器均可,比如:id、.class等 up : { contentrefresh : "正在加载...",//可选,正在加载状态时,上拉加载控件上显示的标题内容 contentnomore:'没有更多数据了',//可选,请求完毕若没有更多数据时显示的提醒内容; callback : fn //必选,刷新函数,根据具体业务来编写,比如通过ajax从服务器获取新数据; } } });
    第四步:设置执行函数
    function fn() { //业务逻辑代码,比如通过ajax从服务器获取新数据; ...... //注意,加载完新数据后,必须执行如下代码,true表示没有更多数据了, 两个注意事项: //1、若为ajax请求,则需将如下代码放置在处理完ajax响应数据之后 // 2、注意this的作用域,若存在匿名函数,需将this复制后使用 var _this = this; _this.endPullupToRefresh(true|false); }
    上拉下拉整合第一步,第二步和下拉刷新一样
    第三步:在mui.init()内同时设置上拉加载和下拉刷新
    mui.init({ pullRefresh: { container: '#pullrefresh', down: { contentdown : "下拉可以刷新",//可选,在下拉可刷新状态时,下拉刷新控件上显示的标题内容 contentover : "释放立即刷新",//可选,在释放可刷新状态时,下拉刷新控件上显示的标题内容 contentrefresh : "正在刷新...",//可选,正在刷新状态时,下拉刷新控件上显示的标题内容 callback: downFn // 下拉执行函数 }, up: { contentrefresh: '正在加载...', callback: upFn // 上拉执行函数 } }});
    注意: 给获取元素加onclick点击事件不行,需要加addEventListener自定义事件
    手势在开发移动端的应用时,会用到很多的手势操作,比如滑动、长按等,为了方便开放者快速集成这些手势,mui内置了常用的手势事件,目前支持的手势事件见如下列表:



    分类
    参数描述




    点击



    tap
    单击屏幕


    doubletap
    双击屏幕


    长按



    longtap
    长按屏幕


    hold
    按住屏幕


    release
    离开屏幕


    滑动



    swipeleft
    向左滑动


    swiperight
    向右滑动


    swipeup
    向上滑动


    swipedown
    向下滑动


    拖动



    dragstart
    开始拖动


    drag
    拖动中


    dragend
    拖动结束



    mui.init({ gestureConfig:{ tap: true, //默认为true doubletap: true, //默认为false longtap: true, //默认为false swipe: true, //默认为true drag: true, //默认为true hold:false,//默认为false,不监听 release:false//默认为false,不监听 } });
    注意:dragstart、drag、dragend共用drag开关,swipeleft、swiperight、swipeup、swipedown共用swipe开关
    你要监听的对象.addEventListener("swipeleft",function(){ console.log("你正在向左滑动"); });
    遮罩在popover、侧滑菜单等界面,经常会用到蒙版遮罩;比如popover弹出后,除popover控件外的其它区域都会遮罩一层蒙版,用户点击蒙版不会触发蒙版下方的逻辑,而会关闭popover同时关闭蒙版;再比如侧滑菜单界面,菜单划出后,除侧滑菜单之外的其它区域都会遮罩一层蒙版,用户点击蒙版会关闭侧滑菜单同时关闭蒙版。
    遮罩蒙版常用的操作包括:创建、显示、关闭,如下代码:
    var mask = mui.createMask(callback);//callback为用户点击蒙版时自动执行的回调; mask.show();//显示遮罩mask.close();//关闭遮罩遮罩css样式: .mui-backdrop
    滑动导航选择mui提供了图片轮播、可拖动式图文表格、可拖动式选项卡、左右滑动9宫格组件,这些组件都用到了mui框架的slide插件,有较多共同点。首先,Dom内容构造基本相同,都必须有一个.mui-slider的父容器;其次,当拖动切换显示内容时,均会触发slide事件(可拖动式选项卡在点击选项卡标题时,也会触发slide事件),通过该事件的detail.slideNumber参数可以获得当前显示项的索引(第一项索引为0,第二项为1,以此类推),利用该事件,可在显示内容切换时,动态处理一些业务逻辑。
    HTML部分:
    <div class="mui-slider"> <!--选项卡标题区--> <div class="mui-slider-indicator mui-segmented-control mui-segmented-control-inverted"> <a class="mui-control-item" href="#item1">待办公文</a> <a class="mui-control-item" href="#item2">已办公文</a> <a class="mui-control-item" href="#item3">全部公文</a> </div> <div class="mui-slider-progress-bar mui-col-xs-4"></div> <div class="mui-slider-group"> <!--第一个选项卡内容区--> <div id="item1" class="mui-slider-item mui-control-content mui-active"> <ul class="mui-table-view"> <li class="mui-table-view-cell">待办公文1</li> <li class="mui-table-view-cell">待办公文2</li> <li class="mui-table-view-cell">待办公文3</li> </ul> </div> <!--第二个选项卡内容区,页面加载时为空--> <div id="item2" class="mui-slider-item mui-control-content"><ul class="mui-table-view"> <li class="mui-table-view-cell">待办公文1</li> <li class="mui-table-view-cell">待办公文2</li> <li class="mui-table-view-cell">待办公文3</li> </ul></div> <!--第三个选项卡内容区,页面加载时为空--> <div id="item3" class="mui-slider-item mui-control-content"></div> </div></div>
    JavaScript部分
    var item2Show = false,item3Show = false;//子选项卡是否显示标志document.querySelector('.mui-slider').addEventListener('slide', function(event) { if (event.detail.slideNumber === 1&&!item2Show) { //切换到第二个选项卡 //根据具体业务,动态获得第二个选项卡内容; var content = 'er'; //显示内容 document.getElementById("item2").innerHTML = content; //改变标志位,下次直接显示 item2Show = true; } else if (event.detail.slideNumber === 2&&!item3Show) { //切换到第三个选项卡 //根据具体业务,动态获得第三个选项卡内容; var content = 'san'; //显示内容 document.getElementById("item3").innerHTML = content; //改变标志位,下次直接显示 item3Show = true; }});
    图片轮播支持循环
    HTML部分:
    <div class="mui-slider"> <div class="mui-slider-group mui-slider-loop"> <!--支持循环,需要重复图片节点--> <div class="mui-slider-item mui-slider-item-duplicate"><a href="#"><img src="images/2.jpg" /></a></div> <div class="mui-slider-item"><a href="#"><img src="images/0.jpg" /></a></div> <div class="mui-slider-item"><a href="#"><img src="images/1.jpg" /></a></div> <div class="mui-slider-item"><a href="#"><img src="images/2.jpg" /></a></div> <!--支持循环,需要重复图片节点--> <div class="mui-slider-item mui-slider-item-duplicate"><a href="#"><img src="images/0.jpg" /></a></div> </div></div>
    不支持循环和循环不同的是没有再第一条和最后一条后面加入内容
    HTML部分:
    <div class="mui-slider"> <div class="mui-slider-group"> <div class="mui-slider-item"><a href="#"><img src="images/0.jpg" /></a></div> <div class="mui-slider-item"><a href="#"><img src="images/1.jpg" /></a></div> <div class="mui-slider-item"><a href="#"><img src="images/2.jpg" /></a></div> <!--<div class="mui-slider-item"><a href="#"><img src="4.jpg" /></a></div>--> </div></div>
    JavaScript部分相同:
    //获得slider插件对象var gallery = mui('.mui-slider');gallery.slider({ interval:5000//自动轮播周期,若为0则不自动播放,默认为0;});document.querySelector('.mui-slider').addEventListener('slide', function(event) { //注意slideNumber是从0开始的; alert("你正在看第"+(event.detail.slideNumber+1)+"张图片");});
    注意:如果ajax获得图片后,需要在写入图片以后,需要从新调用一下
    gallery.slider();
    自定义导航第一步:将一下代码写在header(mHeader) 和 content(mBody) 之间
    首页 科技 娱乐 财经 北京 军事 社会 汽车 视频 美女
    第二步:引入writebug_nav.css 和writebug_nav.js
    第三步:执行函数
    writebug_nav(function(index,data){ // index 为点击索引 data为点击导航的文本内容 console.log(index); console.log(data); });
    Ajax-get请求// get测试请求地址 http://test.write-bug.com/link_app/get?state=index&num=0mui.get('接口地址',{ //请求接口地址 state:'index' // 参数 键 :值 num:'0' },function(data){ // data为服务器端返回数据 //获得服务器响应 ... console.log(data); },'json' );
    Ajax-post请求// post测试请求地址 http://test.write-bug.com/link_app/postmui.post('接口地址',{ //请求接口地址 state:'index', // 参数 键 :值 num:'0' }, function(data){ //data为服务器端返回数据 //自己的逻辑 },'json');
    照相机var cmr = plus.camera.getCamera();cmr.captureImage( function ( p ) { //成功 plus.io.resolveLocalFileSystemURL( p, function ( entry ) { var img_name = entry.name;//获得图片名称 var img_path = entry.toLocalURL();//获得图片路径 }, function ( e ) { console.log( "读取拍照文件错误:"+e.message ); } ); }, function ( e ) { console.log( "失败:"+e.message );}, {filename:'_doc/camera/',index:1} ); // “_doc/camera/“ 为保存文件名
    访问相册plus.gallery.pick( function(path){ img.src = path;//获得图片路径}, function ( e ) { console.log( "取消选择图片" );}, {filter:"image"} );
    蜂鸣提示音switch ( plus.os.name ) { //判断设备类型 case "iOS": if ( plus.device.model.indexOf("iPhone") >= 0 ) { //判断是否为iPhone plus.device.beep(); console.log = "设备蜂鸣中..."; } else { console.log = "此设备不支持蜂鸣"; } break; default: plus.device.beep(); console.log = "设备蜂鸣中..."; break;}
    手机震动switch ( plus.os.name ) { //判断设备类型 case "iOS": if ( plus.device.model.indexOf("iPhone") >= 0 ) { //判断是否为iPhone plus.device.vibrate(); console.log("设备振动中..."); } else { console.log("此设备不支持振动"); } break; default: plus.device.vibrate(); console.log("设备振动中..."); break;}
    弹出菜单弹出菜单的原理主要是通过锚点进行的,如果需要多个弹出菜单,可以在a标签内设置锚点,对应相应的div的id即可
    <a href="#popover">打开弹出菜单</a> // href 定义锚点<div id="popover" class="mui-popover"> //id 对应锚点 <ul class="mui-table-view"> <li class="mui-table-view-cell"><a href="#">Item1</a></li> <li class="mui-table-view-cell"><a href="#">Item2</a></li> <li class="mui-table-view-cell"><a href="#">Item3</a></li> <li class="mui-table-view-cell"><a href="#">Item4</a></li> <li class="mui-table-view-cell"><a href="#">Item5</a></li> </ul></div>
    设备信息plus.device.model //设备型号plus.device.vendor //设备的生产厂商plus.device.imei // IMEI 设备的国际移动设备身份码plus.device.uuid // UUID 设备的唯一标识// IMSI 设备的国际移动用户识别码var str = '';for ( i=0; i<plus.device.imsi.length; i++ ) { str += plus.device.imsi[i];}plus.screen.resolutionWidth*plus.screen.scale + " x " + plus.screen.resolutionHeight*plus.screen.scale ; //屏幕分辨率plus.screen.dpiX + " x " + plus.screen.dpiY; //DPI每英寸像素数
    手机信息plus.os.name //名称plus.os.version //版本plus.os.language //语言plus.os.vendor //厂商//网络类型var types = {};types[plus.networkinfo.CONNECTION_UNKNOW] = "未知";types[plus.networkinfo.CONNECTION_NONE] = "未连接网络";types[plus.networkinfo.CONNECTION_ETHERNET] = "有线网络";types[plus.networkinfo.CONNECTION_WIFI] = "WiFi网络";types[plus.networkinfo.CONNECTION_CELL2G] = "2G蜂窝网络";types[plus.networkinfo.CONNECTION_CELL3G] = "3G蜂窝网络";types[plus.networkinfo.CONNECTION_CELL4G] = "4G蜂窝网络";var network = types[plus.networkinfo.getCurrentType()];
    发送短信<a href=“sms:10086">发送短信var msg = plus.messaging.createMessage(plus.messaging.TYPE_SMS);msg.to = ['13800138000', '13800138001'];msg.body = 'WRITE-BUG https://www.write-bug.com';plus.messaging.sendMessage( msg );
    拨打电话<a href="tel:10086">拨打电话</a>
    发送邮件<a href="mailto:write-bug@writebug.com">发送邮件到WRITE-BUG</a>
    本地存储//设置plus.storage.setItem('键','值'); -> plus.storage.setItem('name','writebug'); //查询plus.storage.getItem('键'); -> var name = plus.storage.getItem('name'); //删除plus.storage.removeItem('键'); -> plus.storage.removeItem('name'); //全部清除plus.storage.clear(); //HTML5自带 - 设置localStorage.setItem('键','值'); -> localStorage.setItem('name','writebug'); //HTML5自带 - 查询localStorage.getItem('键'); -> var name = localStorage.setItem('name');//HTML5自带 - 删除localStorage.removeItem('键'); -> localStorage.removeItem('name');
    图片上传//初始上传地址 var server="http://test.write-bug.com/upload_file.php"; var files=[]; //图片存放的数组 可以上传一个,或者多个图片 //上传图片function upload_img(p){ //又初始化了一下文件数组 为了支持我的单个上传,如果你要一次上传多个,就不要在写这一行了 //注意 files=[]; var n=p.substr(p.lastIndexOf('/')+1); files.push({name:"uploadkey",path:p}); //开始上传 start_upload(); } //开始上传function start_upload(){ if(files.length<=0){ plus.nativeUI.alert("没有添加上传文件!"); return; } //原生的转圈等待框 var wt=plus.nativeUI.showWaiting(); var task=plus.uploader.createUpload(server, {method:"POST"}, function(t,status){ //上传完成 alert(status); if(status==200){ //资源 var responseText = t.responseText; //转换成json var json = eval('(' + responseText + ')'); //上传文件的信息 var files = json.files; //上传成功以后的保存路径 var img_url = files.uploadkey.url; //ajax 写入数据库 //关闭转圈等待框 wt.close(); }else{ console.log("上传失败:"+status); //关闭原生的转圈等待框 wt.close(); } }); task.addData("client",""); task.addData("uid",getUid()); for(var i=0;i<files.length;i++){ var f=files[i]; task.addFile(f.path,{key:f.name}); } task.start(); } // 产生一个随机数function getUid(){ return Math.floor(Math.random()*100000000+10000000).toString();}
    地理位置直接获取地理位置
    plus.geolocation.getCurrentPosition( geoInfo, function ( e ) { alert( "获取位置信息失败:"+e.message );} );
    监听地理位置
    var watchId; //开关 函数外层定义 if ( watchId ) { return; } watchId = plus.geolocation.watchPosition( function ( p ) { alert( "监听位置变化信息:" ); geoInfo( p ); }, function ( e ) { alert( "监听位置变化信息失败:"+e.message ); });
    停止监听地理位置
    if ( watchId ) { alert( "停止监听位置变化信息" ); plus.geolocation.clearWatch( watchId ); watchId = null; }//获得具体地理位置//获取设备位置信息 function geoInfo(position){ var timeflag = position.timestamp;//获取到地理位置信息的时间戳;一个毫秒数; var codns = position.coords;//获取地理坐标信息; var lat = codns.latitude;//获取到当前位置的纬度; var longt = codns.longitude;//获取到当前位置的经度 var alt = codns.altitude;//获取到当前位置的海拔信息; var accu = codns.accuracy;//地理坐标信息精确度信息; var altAcc = codns.altitudeAccuracy;//获取海拔信息的精确度; var head = codns.heading;//获取设备的移动方向; var sped = codns.speed;//获取设备的移动速度; //百度地图申请地址// http://lbsyun.baidu.com/apiconsole/key// http://api.map.baidu.com/geocoder/v2/?output=json&ak=你从百度申请到的Key&location=纬度(Latitude),经度(Longitude)// http://api.map.baidu.com/geocoder/v2/?output=json&ak=BFd9490df8a776482552006c538d6b27&location=40.065639,116.419413 //详细地址 //http://api.map.baidu.com/geocoder/v2/?ak=eIxDStjzbtH0WtU50gqdXYCz&output=json&pois=1&location=40.065639,116.419413 var baidu_map = "http://api.map.baidu.com/geocoder/v2/?output=json&ak=BFd9490df8a776482552006c538d6b27&location="+lat+','+longt; mui.get(baidu_map,{ //请求的地址 }, function(data){ //服务器返回响应,根据响应结果,分析是否登录成功; ... var result = data['result'].addressComponent; // 国家 var country = result['country']; //城市 var city = result['city'];; //地址 var address = result['province'] + result['district'] + result['street']; //data 有很多信息,大家如果需要可以for in出来看下 },'json' ); }
    设置IOS状态栏mui.plusReady(function(){ if(mui.os.ios){ //UIStatusBarStyleDefault //字体深色 //UIStatusBarStyleBlackOpaque //字体浅色 plus.navigator.setStatusBarStyle('UIStatusBarStyleBlackOpaque'); plus.navigator.setStatusBarBackground("#007aff"); //背景颜色 }})
    手机通讯录mui.plusReady(function(){ //访问手机通讯录 plus.contacts.ADDRESSBOOK_PHONE //访问SIM卡通讯录 plus.contacts.ADDRESSBOOK_SIM plus.contacts.getAddressBook(plus.contacts.ADDRESSBOOK_PHONE,function(addressbook){ addressbook.find(null,function (contacts){ for(var a in contacts){ //这里是安卓手机端的获取方式 ios的不太一样,如果需要这块代码可以联系老师获得 var user = contacts[a].displayName; //联系人 var phone = contacts[a].phoneNumbers[0].value; //手机号码 } },function ( e ) {alert( "Find contact error: " +e.message );},{multi:true}); }); });
    启动页设置设置手动关闭启动页
    manifest.json -> plus -> autoclose 改为 false关闭启动页
    plus.navigator.closeSplashscreen();
    PHP后台搭建在开发工具内下载 AppServ 和 ThinkPHP,AppServ是本地服务器,ThinkPHP是后台框架
    ThinkPHP采用单入口模式 index -> 控制器 -> 方法index.php 内书写如下:define("APP_NAME",'WEB'); //站点名称define("APP_PATH",'./WEB/'); //站点路径define("APP_DEBUG",true);//开启调试模式require("./ThinkPHP/ThinkPHP.php");// 引入框架文件
    JSON转换JSON.parse()和JSON.stringify()1.parse 用于从一个字符串中解析出json 对象。例如var str='{"name":"zxd学院","age":"23"}'经 JSON.parse(str) 得到:Object: age:"23" name:"zxd学院"ps:单引号写在{}外,每个属性都必须双引号,否则会抛出异常 2.stringify用于从一个对象解析出字符串,例如 var a={a:1,b:2} 经 JSON.stringify(a)得到: '{"a":1,"b":2}'
    隐藏本页面中滚动条var self = plus.webview.currentWebview();self.setStyle({ bounce: 'none', //禁止弹动 scrollIndicator: 'none' //隐藏滚动条});
    首次启动欢迎页首先引入writebug_welcome.css 和 writebug_welcome.js 文件
    <div id="slider" class="mui-slider" > <div class="mui-slider-group"> <!-- 第一张 --> <div class="mui-slider-item"> <img src="img/shuijiao.jpg"> </div> <!-- 第二张 --> <div class="mui-slider-item"> <img src="img/muwu.jpg"> </div> <!-- 第三张 --> <div class="mui-slider-item"> <img src="img/cbd.jpg"> </div> <!-- 第四张 --> <div class="mui-slider-item"> <img src="img/yuantiao.jpg"> <button id="dy_enter">立即进入</button> </div> </div> <div class="mui-slider-indicator"> <div class="mui-indicator mui-active"></div> <div class="mui-indicator"></div> <div class="mui-indicator"></div> <div class="mui-indicator"></div> </div></div>
    writebug_welcome({ preLoadUrl:'main.html',//预加载页面url preLoadId:'main',//预加载页面id});
    数据库增删改查和接口Class UserAction extends Action { /** * 添加数据 */ public function add(){ $data['phone'] = '1380013800'; $data['name'] = 'yidong'; // M = model M('你要操作的数据表')->方法 $re = M('user')->add($data); //输出 echo $re; // 添加数据返回值 是数据的id } /** * 修改数据 */ public function mod(){ $data['phone'] = '130013000'; $id = 1; $re = M('user')->where("`id`='$id'")->save($data); echo $re; //修改数据 返回值为1是成功 0为失败 } /** * 删除数据 */ public function del(){ $id = '2'; $re = M('user')->where("`id`='$id'")->delete(); echo $re; // 删除 返回值为1也是成功 0 为失败 } /** * 查询数据 */ public function select(){ //单条带条件查询 $id = '1'; $arr1 = M('user')->where("`id`='$id'")->find(); // dump($arr1); // 多条不带条件查询 查询数据库内所有的数据 不建议使用 $arr2 = M('user')->select(); // dump($arr2); // 多条带条件查询 $phone = '1380013800'; $arr3 = M('user')->where("`phone`='$phone'")->select(); // dump($arr3); // 排序 // asc 正序 // desc 倒序 $arr4 = M('user')->where("`phone`='$phone'")->order("id desc")->select(); // dump($arr4); // 分页 limit // limit(参数1); 一个参数的情况下 拿多少条数据 // limit(参数1,参数2); 二个参数的情况下 第一个参数是从多少条开始拿,第二个参数还是拿多少条 // $arr5 = M('user')->order("id desc")->limit(2)->select(); // dump($arr5); $arr6 = M('user')->order("id desc")->limit(2,2)->select(); // dump($arr6); //返回json数据 给我们APP echo json_encode($arr6); // 接口地址 // http://127.0.0.1/www/xianshang14/index.php/User/select }}
    推送注册个推,获得APPKEY、APPID、MASTERSECRET
    推送信息必须打包安装手机后才能使用,主要是通过client_id来进行对每个用户进行推送,首先我们需要在数据库的用户表内添加一个client_id 的字段(在用户注册的时候或者在每次登录的时候存入用户的新client_id,保证推送的有效性),为存放我们用户的client_id,比如这里是个商城,你购买完商品,系统会推送一条信息给你,你只需要告诉程序,你要推送人的手机号码,标题,内容即可(如需要点击信息到达订单页面,需要用透传来实现),服务器获得手机号码以后会在数据库内查找,并获得该用户的client_id,然后实现推送。这里要根据自己的情况来写逻辑,比如WRITE-BUG课堂的分类,前端,后端,数据库等分类,如果我有一个课程上线,我可以推送给这些对某一类感兴趣的学员。当然更多的逻辑需要你自己来写,群发我们可以理解成,循环发送多个单条的(单条发送已经测试没问题,群发没测试,大家可以自己测试一下,有问题随时反馈过来)。
    由于推送信息的多样性,本次封装仅对本APP注册用户进行推送,如需要全员推送,可直接使用个推官网创建信息的方式直接推送。
    推送步骤:

    右上角下载推送包
    single.php (推送单个普通推送/可透传,点击信息可打开APP,透传可写逻辑,透传需要) (透传格式:{“path”:“course”,id:“2”}openPath.php (推送打开页面信息,点击信息可在浏览器打开你传入的URL)download.php (推送下载信息,点击信息可下载你传入URL的文件)
    简单粗暴的设置一下这3个文件内的14行APPKEY,15行APPID,16行MASTERSECRET为你在个推得到的APPKEY、APPID、MASTERSECRET

    如下我只写了一个实例,单条普通信息推送。
    PHP端代码:
    在PHP Action文件夹内建立了一个 PushAction.class.php 的文件
    Class PushAction extends Action { //单个信息推送 透传 public function single(){ $title = $_GET['title_data']; $content = $_GET['content_data']; $phone = $_GET['phone_data']; $pass = $_GET['pass_data']; if($title == '' || $content == '' || $phone == ''){ exit; } $user = M('user')->where("`phone`='$phone'")->find(); $cid = $user['client_id']; $url = 'http://' .$_SERVER['HTTP_HOST'] . . '/Push/single?title='.$title.'&content='.$content.'&cid='.$cid.'&pass='.$pass; $html = file_get_contents($url); echo $html; }}
    APP端代码 我在index文件中:
    // 监听在线消息事件plus.push.addEventListener( "receive", function( msg ) { if ( msg.aps ) { // Apple APNS message// alert( "接收到在线APNS消息:" ); } else {// alert( "接收到在线透传消息:" ); } var login_phone = localStorage.getItem('你存入的登录信息'); var content = msg.content; var json = eval('('+content+')'); var path = json.path; var id = json.id; //订单 if(path == 'order'){ if(login_phone){ dui.jump('./Home/order.html','order'); } }else if(path == 'course'){ localStorage.setItem('writebug_cid',id); dui.jump('./Course/course_detail.html','course_detail'); }else if(path == 'message'){ if(login_phone){ if(id == 'system'){ dui.jump('./Message/system_message.html','system_message'); }else{ dui.jump('./Message/chat_message.html','chat_message'); } } } }, false );
    以上PHP代码可以配合后台,给特定人群推送,逻辑需要大家实现了,因为每个APP的逻辑都不一样
    浏览器打开新页面plus.runtime.openURL( url );
    PDF浏览IOS端内可以直接打开
    安卓端方式

    调用本地第三方浏览器打开
    mui.plusReady(function(){ plus.runtime.openFile( "./file/node_js.pdf" ); });
    引入第三方js类打开

    自定义下拉刷新双webview写到父页面里面
    .mui-pull-top-pocket{ top:100px !important; position: absolute;}.mui-pull-caption { background: red;; background-size: contain; background-repeat: no-repeat; background-position: center; width: 144px; height: 31px; font-size: 0px;}/*下拉刷新圆形进度条的大小和样式*/.mui-spinner { width: 32px; height: 32px;}.mui-spinner:after { background: red;}/*下拉刷新的箭头颜色*/.mui-icon-pulldown { color: #FF058B;}
    即时聊天即时聊天采用野狗无后端模式,野狗: https://www.wilddog.com/
    引入文件
    <script src = "https://cdn.wilddog.com/js/client/current/wilddog.js" ></script>
    写入数据
    // new Wilddog message 为自己定义的一个表或者空间,用于放我们的聊天记录var wd = new Wilddog('https://write-bug.wilddogio.com/message');btn.addEventListener('tap',function(){ var content = text.value; //记录发布时间戳 var date = new Date(); var time = date.getTime(); //插入数据 //第一个参数单独的一个空间,比如两个人聊天,他们就是在单独的一个空间聊天, message 里面可以有很多个独立的空间,比如 张三和李四 是一个空间 王五和赵六又是一个空间 //第二个参数是你发布信息的时间,我们以时间作为信息的依据,通过时间的排序我们的聊天记录 //第三个参数是一个json,为我们的聊天信息,比如 昵称,头像,内容,表情,时间 wd.child('1').child(time).set({ 'name':'write-bug', 'content':content, 'time':time// ...更多 });})
    获得数据
    // 监听聊天内容变化var listen = "https://write-bug.wilddogio.com/message/1";var listen_wd = new Wilddog(listen);listen_wd.on('child_added',function(data){ list.innerHTML += '' +' '+data.val().name+' '+data.val().time+'' +' '+data.val().content+'' +''; console.log(data.val().name);})
    删除
    //1为空间名,1442293959023为某一条信息var ref = new Wilddog("https://writ-ebug.wilddogio.com/message/1/1442293959023");ref.remove()
    时间转换函数
    function getLocalTime(nS) { var mydate = new Date(nS); var today = '';// today += mydate.getFullYear() + '年'; //返回年份// today += mydate.getMonth()+1 + '月'; //返回月份,因为返回值是0开始,表示1月,所以做+1处理// today += mydate.getDate() + '日'; //返回日期 today += mydate.getHours() + ':'; if(mydate.getMinutes() < 10){ var min = '0'+mydate.getMinutes(); }else{ var min = mydate.getMinutes(); } today += min + ':'; today += mydate.getSeconds(); return today;}
    设置滚动条高度
    document.body.scrollTop = document.body.offsetHeight;
    双击安卓返回键退出//监听安卓返回键var first = null;mui.back = function() { if (!first) { first = new Date().getTime(); mui.toast('再按一次退出应用'); setTimeout(function() { first = null; }, 1000); } else { if (new Date().getTime() - first < 1000) { plus.runtime.quit(); } }}
    QQ登录
    申请各个开发平台的开发者:

    微信: https://open.weixin.qq.com/QQ: http://open.qq.com/微博: http://open.weibo.com/
    设置 manifest.json -> SDK配置
    初始化QQ登录、微信登录、微博登录

    var auths={};mui.plusReady(function(){ // 获取登录认证通道 plus.oauth.getServices(function(services){ for(var i in services){ var service=services[i]; auths[service.id]=service; } },function(e){ outLine("获取登录认证失败:"+e.message); });});
    调用认证事件
    // id 为 qq,weixin,weibo function login(id){ console.log("----- 登录认证 -----"); var auth=auths[id]; if(auth){ var w=plus.nativeUI.showWaiting(); document.addEventListener("pause",function(){ setTimeout(function(){ w&&w.close();w=null; },2000); }, false ); auth.login(function(){ w&&w.close();w=null; console.log("登录认证成功:"); console.log(JSON.stringify(auth.authResult)); userinfo(auth); },function(e){ w&&w.close();w=null; console.log("登录认证失败:"); console.log("["+e.code+"]:"+e.message); plus.nativeUI.alert("详情错误信息请参考授权登录(OAuth)规范文档:http://www.html5plus.org/#specification#/specification/OAuth.html",null,"登录失败["+e.code+"]:"+e.message); }); }else{ console.log("无效的登录认证通道!"); plus.nativeUI.alert("无效的登录认证通道!",null,"登录"); }}// 获取用户信息function userinfo(a){ console.log("----- 获取用户信息 -----"); a.getUserInfo(function(){ console.log("获取用户信息成功:"); console.log(JSON.stringify(a.userInfo)); var nickname=a.userInfo.nickname||a.userInfo.name; plus.nativeUI.alert("欢迎“"+nickname+"”登录!"); },function(e){ console.log("获取用户信息失败:"); console.log("["+e.code+"]:"+e.message); plus.nativeUI.alert("获取用户信息失败!",null,"登录"); });}// 注销登录function logoutAll(){ console.log("----- 注销登录认证 -----"); for(var i in auths){ logout(auths[i]); }}function logout(auth){ auth.logout(function(){ outLine("注销\""+auth.description+"\"成功"); },function(e){ outLine("注销\""+auth.description+"\"失败:"+e.message); });}
    5 留言 2019-03-06 21:40:56 奖励36点积分
  • 基于OpenCV实现的自动扫雷机

    先说结果吧,初级和中级都可以在1s内完成游戏,高级经过多次测试,最快4秒,最慢6秒左右。
    然后汇报一下功能,嘛,就自动扫雷呗,可以在游戏开始的时候自动识别总雷数,游戏的规模大小,然后没啥别的特点了。。
    然后简单介绍一下原理,首先自动扫雷首先就要获得游戏的状态,就是什么位置上现在的示数是有还是无,有的话是多少,获取这个主要有两种方法,第一种是外挂法,就是读取扫雷这个进程的内存单元,扫描后可以通过多次试验知道内存状态的含义,然后就可以开始扫了,这种方法我看到有人是这么干的了,这种方法只要会读取/扫描内存,知道内存的含义,额,之后就毫无技术含量了,因为那些还没开的雷的状态你都知道了。。不管规模多大都可以直接秒杀。。然后第二种是我这里用的方法,获取窗口的图像,通过图像来获取游戏的相关信息,进行推理,然后控制鼠标到相应位置完成左键或者右键的点击,在一个大循环中不断地获取图片的状态然后就可以不断的推理,点击了,就趣味性而言,我觉得这种方法比较好玩儿~
    然后下面就是详细讲一下我实现的方法:
    准备工作,作为图像处理,首先要准备一下基本图像,比如不同数字打开后的图片神马的,唉,你知道我试验出数字8那个图像花了多少时间么??在做模板匹配的时候一般不会有人把模板存为jpeg吧。。

    首先是如何获取游戏的窗口的图像,额,我以前的API实现过这个功能,只不过是把图像存到OPENCV的IplImage这种数据类型下而已~
    好,接下来就是要获取雷数,我们知道游戏刚开始的时候左上角显示的就是这盘的总雷数,比如说下图中的40:

    获取这个数字自然也是通过获取这一块的图像来获得啦,由于无论如何都是3位数,所以这个小窗口的大小是不会变的,实验获得这个窗口的位置,宽高什么的都不是难事儿(我这里是通过定位黑色边框找到的,就是下图中最外层的那一圈黑,不过我获取一次后就把位置当常量记下来了。。之后变成默认我知道这个位置。。)

    看上图我们知道每个数字其实就是一个7SEG数码管,亮和暗很容易区分出来,所以我们只要扫描每一条的像素值,就可以知道这个位置是亮还是暗了,把数据存到一个3*7的矩阵中,之后直接译码即可。关于扫描,参见下图:

    横着扫描时可以通过在1/4和3/4位置横扫两次就可以了,竖着也差不多,只要用上数学上的取模运算,可以把for的次数讲到最低。
    然后获取游戏的规模,额,坑爹啊,我实现的方法很简单,就是用上面那个图像库中unknown.bmp去匹配,然后有多少个匹配的就可以知道游戏的规模了,然后写完发现,其实研究一下,就可以通过游戏窗口的大小直接获得游戏的规模了。。不过我那个方法可以在匹配的过程中记录下每个格子相对于游戏窗口的像素坐标,后面点击操作的时候可以直接用,(其实另外一个方法研究一下规律也可以。。)具体的这里就不详述了。。很简单的。。
    接下来需要从图片中获取格子数字,我用了一个比较取巧的方法,我们可以看到每个数字都是由一种颜色写成的,而且每个数字颜色还不一样,所以我们可以在每个格子中间周围找一下背景颜色以外的颜色,如果有1-8中某个数字的颜色就可以知道那个格子是什么数字了。这个方法很快,也很好用,但是细细分析,会有一个不影响使用的缺点,就是数字7是黑色的,而一旦点中雷后,雷的显示也是黑色的,就会把雷判断为数字7,不过嘛,都点出雷了,说明game over了,判断也啥意义,而且我们只要判断游戏上部中间那张脸的样子就可以知道是否点雷了,如果不是,那这个确定就无所谓了。
    如果真有代码的”洁癖”的话,可以用另一种方法来识别,每个格子是16*16 pix的,我们可以记录下每个数字中间那一行的16个点的颜色作为那个数字的”特征向量”,然后在匹配这个就行了,这时候数字7和黑色雷中间一行其实是不一样的,所以不会有这个问题。
    接下来就是扫雷策略上的问题了,很简单,基本是个人都想得到的,就是:

    如果一个格子周围的确定的雷的数目等于这个格子的示数,那么这个格子周围所有还未确定的格子都是安全的
    如果一个格子周围所有不确定的数目等于格子的示数减去已经确定的雷的数目,那么剩余的不确定的格子一定全都是雷

    然后如果这个策略不能确定任何未知格子的状态的话,就随机蒙一个。。
    额,我一开始就是按照这个思路写的,但是实验结果就是,对于初级和中级的游戏,没啥压力,一下子就过关了,但是对于高级,99个雷,棋盘规模有较大,每次都会推无可推,然后随便蒙一个,然后就挂彩了。。我试了很多次,一次都没过关过。。然后我就开始研究一个复杂一点的推导,准本在上面那个初级策略无效的时候采用。
    上面的初级策略的缺点就是只考虑每个格子自身周围的数字而不考虑周围的格子的周围,就是多个格子联立思考,比如说下图:

    细细研究一下就知道这时候初级策略已经没有任何可以确定的未知格子了,但是看下图:

    我们研究红框的那一部分,为了方便说明,对于红框内的部分我们建立新坐标,比如A就是(3,2)。我们看(2,3)那个点,数字2表明了在A,B,C三个点钟还有一个雷,我们再看看这个点上方(1,3)那个点,它表明B和C中有一个雷
    A,B,C中共有一个雷,BC中也只有一个雷,那就是说明A一定不是雷!!
    再比如说下图:

    也是到了初级策略束手无策的时候,我们再看看红框中的部分,点(3,4)的那个数字3表明a,b,c中有两个是雷,而它旁边(3,5)那个点表明bc中只有一个是雷,那就是说,a一定是雷!!
    为了把上面的推导一般化,我们先声明一下几个概念:
    一是公共未知区域,表示(i,j)点周围未知区域中和(i’,j’)周围未知区域中重复的那一部分【(i,j)要和(i’,j’)相邻】,比如说上图中(3,4)和(3,5)公共未知区域就是b,c,因为b,c不仅是(3,4)的周围的未知区域,还是(3,5)周围的未知区域
    二是点(i,j)关于点(i’,j’)的非公共未知区域【(i,j)要和(i’,j’)相邻】,就是说(i,j)周围的未知区域中不是(i’,j’)周围的未知区域的那一些。比如上图中(3,4)关于(3,5)的非公共未知区域就是a,因为a属于(3,4)的未知区域,但是不属于(3,5)
    然后我们要使用上述高级策略的话,必须要确定地知道公共未知区域中雷的数目,这点要怎么保证呢,就是(i’,j’)的未知区域就是公共未知区域,这样我们就可以通过(i’,j’)的示数和周围已经开拓出来的雷的位置知道(i’,j’)周围还有几个雷,而这个雷的数目就是公共未知区域的雷的数目。有了上述概念,高级策略如下:

    如果点(i,j)剩余的地雷数等于(i,j)关于(i’,j’)非公共区域位置的数目加上(i,j)和(i’,j’)的公共未知区域的雷的数目,那么(i,j)关于点(i’,j’)的非公共未知区域则全是雷
    如果点(i,j)剩余的地雷数等于(i,j)关于(i’,j’)非公共区域位置的数目,那么(i,j)关于点(i’,j’)的非公共未知区域则全部安全

    实验结果表明,用了高级策略后,高级模式下无压力,只要不是运气太衰,一般都可以过关。运气太衰有两种表现,分别在开头和结尾,开头就是说你一开始每次随机点的时候都只点出一格,没有出一片来,那这种情况下你只能继续随机点,点着点着雷就爆了。。
    还有另外一种情况就是在结尾的时候,比如说下图。。

    幸好我这是机器扫出来的,输了就输了,如果是人手一个一个扫,扫到最后出现这种情况,直接砸电脑!!(掀桌!!暴怒)不过还真别说,这种情况还挺常见的!!果然游戏生成布局的时候就很不科学。。
    1 留言 2019-05-12 10:18:56 奖励25点积分
  • 摄像头图像采集及拼接程序的实现

    程序的说明实现从摄像头实时采集单帧图像,之后完成图像的拼接,本程序实现了两张图片的拼接和三张图片的拼接。
    在此之前你需要在 linux 下安装 opencv Package 这个包,因为本程序主要使用 opencv 这个包中提供的 api 函数。
    实现从摄像头实时不同视角采集视频的单帧图像并保存实时采集的视频文件之后,完成图像的拼接,由于实验室设备有限,手头只有两个摄像头一次只能抓取。
    两张不同视角的单帧图像,我们抓取的单帧图像保存在当前项目目录下的 frame1 和 frame2 文件夹中,因此我同时制作了两个完成程序。
    拼接的程序,一个实现完成两个不同视角的图像拼接,另一个实现三张不同视角的单帧图像的拼接。其中的 testusb.cpp 文件是测试摄像头的程序。在执行本程序前,你应该保证有两个是摄像头插在主机端口上,用于实时采集单帧图像。
    代码介绍在进行程序的编译前,请确定你已经安装了 opencv2.4.9 和 pkg-config 包,本程序是在 ubuntu14.04 平台下实现的,在本项目目录下,已经有编译生成的可执行程序,其中 Camera_to_Frmae.cpp 是我们从双摄像头实时抓取单帧图像的源码。

    ImageJoint.cpp 和 ImageJoint2.cpp、ImageJoint3.cpp 分别是完成两张不同视角的图像拼接和三张不同视角的图像拼接程序,其中三张图像拼接的图像是我从网上找的现成的图像库
    testusb.cpp 是我测试摄像头的程序

    程序编译g++ -o dst src.cpp \`pkg-config opencv --cflags --libs\`
    程序的执行和退出
    ./dst
    程序需要退出时,按 Ctrl + C 快捷键

    效果从摄像头设备采集两张单帧图像


    图像拼接效果图
    2 留言 2019-05-11 16:33:28 奖励11点积分
  • EXE加载模拟器直接在内存中加载运行EXE不通过API创建进程运行 精华

    背景在网上搜索了很多病毒木马的分析报告,看了一段时间后,发现还是有很多病毒木马都能够模拟PE加载器,把DLL或者是EXE等PE文件,直接从内存中直接加载到自己的内存中执行,不需要通过API函数去操作,以此躲过一些杀软的检测。
    在看到这些技术的描述后,虽然没有详细的实现思路,但是凭借自己的知识积累,我也大概知道是怎么做了。后来,就自己动手写了这么一个程序,实现了从内存中直接加载并运行EXE,不需要通过API函数创建另一个进程启动该EXE。暂时还没有想清楚这种技术有什么积极的一面,不管了,既然都把程序写出来了,那就当作是对PE结构以及编程水平的一次锻炼吧
    现在,把实现的思路和实现过程,写成文档,分享给大家。
    程序实现原理要想完全理解透彻这个程序的技术,需要对PE文件格式有比较详细的了解才行,起码要了解PE格式的导入表、导出表以及重定位表的具体操作过程。
    这个程序和 “DLL加载模拟器直接在内存中加载DLL不通过API加载” 这篇文章原理是一样的,只是一个是EXE,一个是DLL,本质上都是PE文件加载模拟器。
    EXE加载到内存的过程并运行实现原理
    首先,在EXE文件中,根据PE结构格式获取其加载映像的大小SizeOfImage,并根据SizeOfImage在自己的程序中申请一块可读、可写、可执行的内存,那么这块内存的首地址就是EXE程序的加载基址
    然后,根据EXE中的PE结构格式获取其映像对齐大小SectionAlignment,然后把EXE文件数据按照SectionAlignment对齐大小拷贝到上述申请的可读、可写、可执行的内存中
    接着,根据PE结构的重定位表,重新对重定位表进行修正
    接着,根据PE结构的导入表,加载所需的DLL,并获取导入表导入函数的地址并写入导入表中
    接着,修改EXE的加载基址ImageBase
    最后,根据PE结构获取EXE的入口地址AddressOfEntryPoint,然后跳转到入口地址处继续执行

    这样,EXE就成功加载到程序中并运行起来了。要注意的一个问题就是,并不是所有的EXE都有重定位表,对于没有重定位表的EXE程序,那就不适用于本文介绍的方法。
    编码实现// 模拟PE加载器加载内存EXE文件到进程中// lpData: 内存EXE文件数据的基址// dwSize: 内存EXE文件的内存大小// 返回值: 内存EXE加载到进程的加载基址LPVOID MmRunExe(LPVOID lpData, DWORD dwSize){ LPVOID lpBaseAddress = NULL; // 获取镜像大小 DWORD dwSizeOfImage = GetSizeOfImage(lpData); // 在进程中开辟一个可读、可写、可执行的内存块 lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (NULL == lpBaseAddress) { ShowError("VirtualAlloc"); return NULL; } ::RtlZeroMemory(lpBaseAddress, dwSizeOfImage); // 将内存PE数据按SectionAlignment大小对齐映射到进程内存中 if (FALSE == MmMapFile(lpData, lpBaseAddress)) { ShowError("MmMapFile"); return NULL; } // 修改PE文件重定位表信息 if (FALSE == DoRelocationTable(lpBaseAddress)) { ShowError("DoRelocationTable"); return NULL; } // 填写PE文件导入表信息 if (FALSE == DoImportTable(lpBaseAddress)) { ShowError("DoImportTable"); return NULL; } //修改页属性。应该根据每个页的属性单独设置其对应内存页的属性。 //统一设置成一个属性PAGE_EXECUTE_READWRITE DWORD dwOldProtect = 0; if (FALSE == ::VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { ShowError("VirtualProtect"); return NULL; } // 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase if (FALSE == SetImageBase(lpBaseAddress)) { ShowError("SetImageBase"); return NULL; } // 跳转到PE的入口点处执行, 函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint if (FALSE == CallExeEntry(lpBaseAddress)) { ShowError("CallExeEntry"); return NULL; } return lpBaseAddress;}
    程序测试在 main 函数中调用上述封装好的函数,加载EXE程序进行测试。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ char szFileName[] = "KuaiZip_Setup_2.8.28.8.exe"; // 打开EXE文件并获取EXE文件大小 HANDLE hFile = CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hFile) { ShowError("CreateFile"); return 1; } DWORD dwFileSize = GetFileSize(hFile, NULL); // 申请动态内存并读取DLL到内存中 BYTE *pData = new BYTE[dwFileSize]; if (NULL == pData) { ShowError("new"); return 2; } DWORD dwRet = 0; ReadFile(hFile, pData, dwFileSize, &dwRet, NULL); CloseHandle(hFile); // 判断有无重定位表 if (FALSE == IsExistRelocationTable(pData)) { printf("[FALSE] IsExistRelocationTable\n"); system("pause"); return 0; } // 将内存DLL加载到程序中 LPVOID lpBaseAddress = MmRunExe(pData, dwFileSize); if (NULL == lpBaseAddress) { ShowError("MmRunExe"); return 3; } system("pause"); return 0;}
    测试结果:
    运行程序后,成功显示“快压”安装程序的对话框界面,而且还显示了“快压”安装程序正在向 “i.kpzip.com” 使用HTTP发送“GET”数据请求:

    所以,程序测试成功。
    总结这个程序你只要熟悉PE格式结构的话,这个程序理解起来会比较容易。其中,需要特别注意的一点是:并不是所有的EXE都是用于本文介绍的方法。因为,对于那些没有重定位表的EXE程序来说,它们加载运行时的加载基址只能是固定的,因为没有重定位表的缘故,所以无法重定位数据,势必不能成功运行程序。同时,对一些MFC程序也不支持,目前正在改进当中。如果遇到不成功的,那就多换几个有重定位表的EXE试试就好。
    参考参考自《Windows黑客编程技术详解》一书
    8 留言 2019-01-02 09:47:41 奖励25点积分
  • Minifilter驱动程序与用户层程序通信 精华

    背景通常 NT 驱动程序与用户层间的通信,可以由用户层调用 CreateFile 函数打开驱动设备并获取设备句柄,然后调用 DeviceIoControl 函数实现用户层数据和内核层数据的交互。
    那么,对于 Minifilter,它是一个 WDM 驱动,它并不像 NT 驱动那样使用常用的方式通信,而是有自己一套函数专门用于数据通信交互。现在,我就把程序的实现过程和原理整理成文档,分享给大家。
    实现过程用户层程序的实现过程导入库文件我们先来介绍下用户层上的程序的实现过程。首先,我们需要包含头文件 fltUser.h 以及库文件 fltLib.lib,这些文件在 VS 中并没有,它们存在于 WDK 中。我们可以设置程序的目录包含路径以及库文件包含路径,也可以将 WDK 中这两个文件拷贝到当前目录中来。我们选择后一种方法,将下面目录下的文件拷贝到当前目录中:

    C:\Program Files (x86)\Windows Kits\8.1\Include\um\fltUser.h
    C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86\fltLib.lib
    C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x64\fltLib.lib

    那么,我们在程序中声明头文件以及导入库文件的代码为:
    #include "flt\\fltUser.h"#ifdef _WIN32 #pragma comment(lib, "flt\\lib\\x86\\fltLib.lib")#else #pragma comment(lib, "flt\\lib\\x64\\fltLib.lib")#endif
    调用函数实现交互用户层上实现于 Minifilter 内核层的数据交互方法,和用户层与 NT 驱动程序的交互方法很相似,虽然不是 CreateFile 打开对象获取句柄,在调用 DeviceIoControl 交互数据。具体的实现步骤如下:

    首先,调用 FilterConnectCommunicationPort 函数打开通信端口,获取端口的句柄
    然后,调用 FilterSendMessage 函数交互数据,向内核程序传入输入、输出缓冲区
    当交互结束,通信句柄不再使用的时候,调用 CloseHandle 函数关闭句柄

    综合上面 3 个步骤来看,是不是和 NT 驱动程序的交互方式很相似呢?我们通过类比记忆就好。其中,Minifilter 是通过端口的方式来实现数据交互的。具体的实现代码如下所示:
    int _tmain(int argc, _TCHAR* argv[]){ HANDLE hPort = NULL; char szInputBuf[MAX_PATH] = "From User Test!"; char szOutputBuf[MAX_PATH] = { 0 }; DWORD dwInputLen = 1 + ::lstrlen(szInputBuf); DWORD dwOutputLen = MAX_PATH; DWORD dwRet = 0; HRESULT hRet = NULL; // 打开并连接端口, 获取端口句柄. (类似CreateFile) hRet = ::FilterConnectCommunicationPort(PORT_NAME, 0, NULL, 0, NULL, &hPort); if (IS_ERROR(hRet)) { ::MessageBox(NULL, "FilterConnectCommunicationPort", NULL, MB_OK); return 1; } // 向端口发送数据. (类似 DeviceIoControl) hRet = ::FilterSendMessage(hPort, szInputBuf, dwInputLen, szOutputBuf, dwOutputLen, &dwRet); // 类似DeviceIoControl if (IS_ERROR(hRet)) { ::MessageBox(NULL, "FilterSendMessage", NULL, MB_OK); return 2; } // 显示数据 printf("InputBuffer:0x%x\n", szInputBuf); printf("OutputBuffer:0x%x\n", szOutputBuf); system("pause"); return 0;}
    内核层程序的实现过程从上面用户层程序的实现过程来看,和通常的交互方式来看,没有什么大区别,只是调用的函数变了而已。但是,对于内核层,却有很大的改变。
    我们知道,VS2013 里面有向导可以直接创建一个 Minifilter 驱动,可以生成代码框架和 inf 文件,这简化了很多工作。但是,VS2013 开发化境并没有帮我们生成与用户层通信部分的代码,所以,需要我们手动对代码进行更改,实现与用户层的数据通信。具体的步骤如下:
    1.首先,在内核程序的顶头声明 2 个全局变量,保存通信用的服务器端口以及客户端端口;并且声明 3 个回调函数:建立连接回调函数、数据通信回调函数、断开连接回调函数。
    // 端口名称#define PORT_NAME L"\\CommPort"// 服务器端口PFLT_PORT g_ServerPort;// 客户端端口PFLT_PORT g_ClientPort;// 建立连接回调函数NTSTATUS ConnectNotifyCallback( IN PFLT_PORT ClientPort, IN PVOID ServerPortCookies, IN PVOID ConnectionContext, IN ULONG SizeOfContext, OUT PVOID *ConnectionPortCokkie);// 数据通信回调函数NTSTATUS MessageNotifyCallback( IN PVOID PortCookie, IN PVOID InputBuffer OPTIONAL, IN ULONG InputBufferLength, OUT PVOID OutputBuffer, IN ULONG OutputBufferLength, OUT PULONG ReturnOutputBufferLength);// 断开连接回调函数VOID DisconnectNotifyCallback(_In_opt_ PVOID ConnectionCookie);
    2.然后,我们来到 DriverEntry 入口点函数,进行修改:

    首先,调用 FltRegisterFilter 注册过滤器
    然后,在使用 FltCreateCommunicationPort 函数创建通信端口之前,需要调用 FltBuildDefaultSecurityDescriptor 函数创建一个默认的安全描述符。其中,FLT_PORT_ALL_ACCESS 表示程序拥有连接到端口、访问端口等所有权限。其中,Minifilter 通常在调用 FltCreateCommunicationPort 函数之前会调用 FltBuildDefaultSecurityDescriptor 函数;在调用完 FltCreateCommunicationPort 函数后,会调用 FltFreeSecurityDescriptor 函数
    接着,调用 FltCreateCommunicationPort 创建通信服务器端口,使得Minifilter 驱动程序可以接收来自用户层程序的连接请求。可以通过该函数设置端口名称、建立连接回调函数、数据通信回调函数、断开连接回调函数、最大连接数等,同时可以获取服务器端口句柄
    然后,调用 FltFreeSecurityDescriptor 函数释放安全描述符
    最后,调用 FltStartFiltering 函数开始启动过滤注册的 Minifilter 驱动程序

    NTSTATUS DriverEntry ( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ){ NTSTATUS status; UNREFERENCED_PARAMETER( RegistryPath ); PT_DBG_PRINT( PTDBG_TRACE_ROUTINES, ("Minifilter_Communicate_Test!DriverEntry: Entered\n") ); // // Register with FltMgr to tell it our callback routines // status = FltRegisterFilter( DriverObject, &FilterRegistration, &gFilterHandle ); FLT_ASSERT( NT_SUCCESS( status ) ); if (NT_SUCCESS( status )) { PSECURITY_DESCRIPTOR lpSD = NULL; // 创建安全描述, 注意:要创建这个安全描述,否则不能成功通信 status = FltBuildDefaultSecurityDescriptor(&lpSD, FLT_PORT_ALL_ACCESS); if (!NT_SUCCESS(status)) { KdPrint(("FltBuildDefaultSecurityDescriptor Error[0x%X]", status)); return status; } // 创建于用户层交互的端口 UNICODE_STRING ustrCommPort; OBJECT_ATTRIBUTES objectAttributes; RtlInitUnicodeString(&ustrCommPort, PORT_NAME); InitializeObjectAttributes(&objectAttributes, &ustrCommPort, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, lpSD); status = FltCreateCommunicationPort(gFilterHandle, &g_ServerPort, &objectAttributes, NULL, ConnectNotifyCallback, DisconnectNotifyCallback, MessageNotifyCallback, 1); if (!NT_SUCCESS(status)) { KdPrint(("FltCreateCommunicationPort Error[0x%X]", status)); return status; } // 释放安全描述 FltFreeSecurityDescriptor(lpSD); // // Start filtering i/o // status = FltStartFiltering( gFilterHandle ); if (!NT_SUCCESS( status )) { FltUnregisterFilter( gFilterHandle ); } } return status;}
    其中,建立连接回调函数的代码为:
    NTSTATUS ConnectNotifyCallback( IN PFLT_PORT ClientPort, IN PVOID ServerPortCookies, IN PVOID ConnectionContext, IN ULONG SizeOfContext, OUT PVOID *ConnectionPortCokkie){ PAGED_CODE(); UNREFERENCED_PARAMETER(ServerPortCookies); UNREFERENCED_PARAMETER(ConnectionContext); UNREFERENCED_PARAMETER(SizeOfContext); UNREFERENCED_PARAMETER(ConnectionPortCokkie); // 可以加以判断,禁止非法的连接,从而给予保护 g_ClientPort = ClientPort; // 保存以供以后使用 return STATUS_SUCCESS;}
    只要有连接连接到端口上,就会调用此函数。我们可以在该回调函数中获取客户端的端口句柄。这个客户端端口句柄要保存下来,这样,我们的驱动程序才可以和建立连接的用户层程序使用该客户端句柄进行数据通信。
    其中,断开连接回调函数的代码为:
    VOID DisconnectNotifyCallback(_In_opt_ PVOID ConnectionCookie){ PAGED_CODE(); UNREFERENCED_PARAMETER(ConnectionCookie); // 应该加判断,如果ConnectionCookie == 我们的值就执行这行 FltCloseClientPort(gFilterHandle, &g_ClientPort);}
    每当有连接断开的时候,就会调用该函数。我们需要在此调用 FltCloseClientPort 函数,关闭客户端端口。
    其中,数据交互回调函数的代码为:
    NTSTATUS MessageNotifyCallback( IN PVOID PortCookie, IN PVOID InputBuffer OPTIONAL, IN ULONG InputBufferLength, OUT PVOID OutputBuffer, IN ULONG OutputBufferLength, OUT PULONG ReturnOutputBufferLength){ /* 这里要注意: 1.数据地址的对齐. 2.文档建议使用:try/except处理. 3.如果是64位的驱动要考虑32位的EXE发来的请求. */ NTSTATUS status = STATUS_SUCCESS; PAGED_CODE(); UNREFERENCED_PARAMETER(PortCookie); /* 这里输入、输出的地址均是用户空间的地址!!! */ // 显示用户传输来的数据 KdPrint(("[InputBuffer][0x%X]%s\n", InputBuffer, (PCHAR)InputBuffer)); KdPrint(("[OutputBuffer][0x%X]\n", OutputBuffer)); // 返回内核数据到用户空间 CHAR szText[] = "From Kernel Data!"; RtlCopyMemory(OutputBuffer, szText, sizeof(szText)); *ReturnOutputBufferLength = sizeof(szText); return status;}
    每当有数据交互的时候,就会调用此回调函数。我们可以从输入缓冲区中获取来自用户层程序传入的数据。然后对输出缓冲区进行设置,将内核数据输出到用户层中。这个函数和 NT 驱动程序中的 IRP_MJ_DEVICE_CONTRL 消息对应的操作函数类似。
    3.当驱动卸载的时候,要在卸载函数中调用
    // 没有这一行是停止不了驱动的,查询也是永远等待中FltCloseCommunicationPort(g_ServerPort);
    否则,停止不了驱动的,查询也是永远等待中。
    程序测试在 Win7 32 位系统下,驱动程序正常执行:

    在 Win10 64 位系统下,驱动程序正常执行:

    总结Minifilter 的通讯结构不难理解,注意和 NT 驱动程序的驱动结构进行类比理解就好。
    要注意该程序的加载,并不像 NT 驱动那样,调用加载程序来加载。WDM驱动,采用 inf 文件的安装方式,但是,一定要注意:MiniFilter生成后,一定要修改 inf中的 Instance1.Altitude = “370030”,即将注释去掉即可。因为每一个 Minifilter 驱动都必须指定一个 Altitude。每一个发组都有自己的一个 Altitude 区间,Altitude 值越高,代表在设备栈里面的位置也越高,也就是越先收到应用层发过来的IRP。
    inf 文件安装驱动方式:

    选中inf文件,鼠标右键,选择“安装”
    安装完毕后,以管理员权限打开cmd,输入“net start 服务名”启动服务
    停止服务则使用命令“net stop 服务名”即可

    同时要注意,程序在判断文件路径的时候,要使用 ExAllocatePool 申请非分页内存,不要直接使用变量,因为使用 FltGetFileNameInformation 获取的路径信息是存储在分页内存中,直接在回调函数中使用会导致蓝屏情况。
    参考参考自《Windows黑客编程技术详解》一书
    2 留言 2019-02-15 17:40:50 奖励12点积分
  • 基于DirectShow实现的视频捕捉与采集程序

    前言DirectShow是微软公司提供的一套在Windows平台上进行流媒体处理的开发包,与DirectX开发包一起发布。DirectShow为多媒体流的捕捉和回放提供了强有力的支持。用DirectShow开发应用程序,我们可以很方便地从支持WDM驱动模型的采集卡上捕获数据,并且进行相应的后期处理乃至存储到文件中。
    DirectShow是基于COM的,为了编写DirectShow应用程序,需要了解COM客户程序编写的基础知识。DirectShow提供了大量的接口,但在编程中发现还是不够方便,如果能构建一个视频捕捉类把常用的一些动作封装起来,那么就更方便了。
    编程思路为了更加容易建立视频捕捉应用程序,DirectShow提供了一个叫做Capture Graph Builder的对象,Capture Graph Builder提供IcaptureGraphBuilder2接口,该接口可以建立和控制Capture Graph。
    建立视频捕捉程序,必须首先获取并初始化IcaptureGraphBuilder2接口,然后选择一个适当的视频捕捉设备。选择好设备后,为该设备创建Capturefilter,然后调用AddFilter把Capture filter添加到Filter Graph。如果仅仅希望用摄像头来进行实时监控的话,只需要在上面的基础上调用ICaptureGraphBuilder2::RenderStream就可以了
    本程序在VS2010 PlatForm采用Micosoft 的DriectXShow9.0b SDK编写的一套实时视频捕捉,采集功能,并且还提供了视频和单帧图像参数设置界面,采集的视频流和单帧图像都写在文件中,之后使用OpenCV进行音视频的处理,当然这是我后期要做的事情,前面部分已经完成,前半部分开发使用了因为很多东西要查,需要学习DXShow如何设计filter graph manger 和filter graph 的创建以及pin 的连接,最后renderstream,产生视频流,之后,在视频流上捕捉单帧图像数据,加入图像头,写入文件进行保存到文件系统.
    环境配置
    首先保证已经正确安装了MicroSoft的DXSDK,请一定安装DriectXShow9.0b版本的SDK包,这里我将其安装了在C盘根目录下
    打开VS2010项目,在项目属性中设置项目需要使用的项目需要包含的包含目录和库目录
    具体步骤:

    项目-项目名属性-配置属性-VC++目录-包含目录-“C:\DXSDK\Include”项目-项目名属性-配置属性-VC++目录-包含目录-“C:\DXSDK\Lib”

    软件效果全局效果

    视频格式设置

    图像格式设置
    1 留言 2019-05-09 16:13:33 奖励16点积分
  • 网络爬虫技术原理介绍 精华

    什么是爬虫?网络爬虫是一种按照一定的规则,自动的爬取万维网信息的程序或者脚本。网络爬虫按照系统结构和实现技术,大致分为通用爬虫,聚焦爬虫,增量式爬虫,深层网络爬虫。但是实际应用中一般都是几种爬虫技术相结合实现的。
    搜索引擎其实是一种大型的复杂的网络爬虫属于通用爬虫的范畴。专业性质的搜索引擎则是属于聚焦爬虫的范畴。增量式爬虫则是对已下载的网页采取增量式更新和只爬取新产生的或者已经发生变化的网页的爬虫。Web网页按照存在的方式可以分为表层网页和深层网页,表层网页通常是指传统引擎可以索引的页面,以超链接可以到达的静态网页为主构成的Web页面;深层网络是那些大部分内容不能通过静态链接获取的、隐藏在搜索表单后的,只有用户提交一些关键词才能获取的Web页面。爬取深层网络页面的爬虫就属于深层网络爬虫的范畴。
    爬虫的工作流程

    首先选取一部分或者一个精心挑选的中字URL
    将这些URL放入带抓取的URL队列
    从待抓取的URL队列中读取待抓取的URL,解析DNS,并且得到主机的IP,并将URL对应的网页下载下来,存储到已下载的网页数据中。并且将这些URL放进已抓取的URL队列中
    分析已抓取URL队列中URL,从已下载的网页数据中分析出其他的URL,并和已抓取的URL进行去重处理,最后将去重后的URL放入待抓取URL队列,从而进入下一个循环

    爬虫的python实现框架Scrapy爬虫的python实现的库有很多,主要有urllib,httplib/urllib,requests.这里主要讲一下Scrapy框架。
    Scrapy使用python写的Crawler Framwork,简单轻巧,并且非常方便。Scrapy使用的是Twisted异步网络库来处理网络通信,架构清晰,并且包含了各种中间件接口,可以灵活的完成各种需求。


    引擎打开一个网站,找到处理改网站的spider并向该spider请求第一个要爬取的URL
    引擎从Spider中获取的第一个要爬取的URL并通过调度器(Scheduler)以Requests进行调度
    引擎向调度器(Scheduler)请求下一个要爬取的URL
    调度器(Scheduler)返回下一个要爬取的URL给引擎(Scrapy Engine),引擎将URL通过下载器中间件(Downloader Middleware)转发给下载器
    一旦页面下载完毕,下载器生成一个该页面的Response,并将其通过下载器中间件转发给引擎
    引擎从下载器中间件接收到Response并通过spider中间件发送给spider处理
    Spider处理Response并返回爬取到的Item和新的Request给引擎
    引擎将爬取到的item给Item Pipeline,将Request给调度器
    从第2步开始重复直到调度器中没有Request,引擎关闭该网站
    6 留言 2019-04-05 15:45:45 奖励15点积分
  • 基于Python实现的新闻网络爬虫程序 精华

    1、简介1.1 引用术语与缩写解释


    缩写、术语
    解 释




    Python
    一种简洁而强大的解释型脚本语言


    pyodbc
    Python下的ODBC数据库访问组件


    SQLAlchemy
    Python下的ORM数据访问组件


    pywin32
    Python下的Win32接口访问组件


    requests
    Python下的Web访问组件


    Pillow
    Python下的图像处理组件


    解释型语言
    无需编译源码可敏捷部署并执行的语言


    IOC
    控制反转,运行时可由数据反向干涉程序执行逻辑的设计模式


    RegularExpression
    正则表达式,查找和替换文本模式的简洁而灵活的表示法


    XML
    扩展标记语言,用于配置及数据交互



    1.2 概要本文档针对以下三个方面进行了详细说明:

    架构说明,对新闻网络爬虫的核心架构进行描述,供开发人员在开发或升级应用时参考
    部署说明,对新闻网络爬虫的部署步骤进行描述,供部署人员进行应用部署或升级时参考
    扩展说明,对新闻网络爬虫的扩展模式进行描述,供开发人员扩展爬虫功能时参考

    1.3 应用设计目标
    易于扩展
    易用
    易于维护

    为了达成这些设计目标我们采用了以下设计机制:

    易于扩展

    使用解释型、面向对象的Python语言实现程序逻辑:解释型语言易于扩展部署,结合抓取模块的IOC机制,更新升级时无需停机;面向对象易于代码复用,创建新爬虫模块时仅需少量重载代码修改控制流程IOC化,利用XML配置文件,可动态增减爬虫任务、控制爬虫任务启动条件、更改爬虫规则以及爬虫逻辑
    易用

    服务化,爬虫任务被封装为Windows 服务,可便捷地启动与停止,于后台执行长期运行
    易于维护

    编程语言简洁,缩进式风格减少了语法性代码,动态数据类型减少了声明性代码,函数式编程特性减少了重复性函数代码,友好而功能强大的标准库与外部组件也减少逻辑复杂性数据访问层模型化:使用SQLAlchemy组件,对数据访问进行对象化建模,屏蔽数据访问细节,如SQL,数据模型易于维护

    2、架构说明新闻网络爬虫程序主题为一系列python脚本,通过文件夹进行模块隔离,基于命名约定进行动态逻辑加载,根目录为ArticleSpider。
    整体框架如下:


    SpiderService.py:服务入口模块,用以处理Windows服务Article Spider Service的安装、卸载、启动、停止与重启
    SpiderTask.py:任务管理模块,负责加载控制规则配置、安排爬虫任务计划、组合爬虫任务子逻辑
    ArticleStorer.py:文章转存模块,包含数据库访问、图片转存与切图、队列消息发送功能
    RuleReader.py:规则读取模块,用于读取爬虫规则,辅助IOC机制
    Spider:爬虫逻辑模块,核心模块群,可根据需要添加新爬虫模板,爬虫模板可继承,基模块为Spider.py,多个相似爬虫可根据规则设置复用同一个爬虫模板
    Model:数据模型模块,维护爬虫相关ORM数据模型,由上下文管理层、数据模型层与事务逻辑层组成
    Message:消息处理模块,主要负责封装与发送队列消息
    SpiderRule.xml:爬虫规则配置,XML格式元数据
    Temp:缓存目录,用以缓存转存完成前的中间文件,如下载图片
    Log:日志目录,用以保存日志,采用循环日志模式
    ServiceCommand.txt:服务入口命令,用于参考的爬虫服务基本人机交互命令
    SpiderTest.py:爬虫测试模块,用于测试的相关脚本

    2.1 模块说明2.1.1 服务入口层 SpiderService.py2.1.1.1 SpiderService
    win32serviceutil.ServiceFramework:服务基类,引入自pywin32组件,用以将Python程序封装位Windows服务
    SvcDoRun:服务启动入口,重载基类方法,用以开启SpiderTask任务管理线程,阻塞等待任务结束事件
    SvcStop:服务终止入口,重载基类方法,用以终止SpiderTask任务管理线程,发起任务结束事件

    2.1.1.2 ServiceCommand
    python SpiderService.py install:爬虫服务安装,必须指令,用以注册Windows服务,安装成功后可直接于Windows服务管理器中进行服务启动、停止等管理操作
    python SpiderService.py install —startup auto:自启动爬虫服务安装
    python SpiderService.py start:启动爬虫服务,服务安装后有效
    python SpiderService.py restart:重启爬虫服务,服务启动后有效
    python SpiderService.py stop:停止爬虫服务,服务启动后有效
    python SpiderService.py remove:删除/卸载爬虫服务,服务安装后有效

    2.1.2 任务管理层 SpiderTask.py2.1.2.1 SpiderTask
    threading.Thread:线程管理基类,引入自python标准库,用以将主任务进行线程化封装
    __init__:初始化方法,进行一系列任务初始化操作,如线程初始化、日志初始化、调度初始化、存储初始化等操作
    ScheduleTask:任务调度方法,读取爬虫规则,根据设置生成爬虫调度计划
    RunSpiderByRule:爬虫驱动方法,按照给定规则,驱动对应爬虫开启任务,核心步骤为,爬虫选用—文章抓取—图片转存—文章入库—后续处理(如压图与消息通知)
    run:任务子线程启动入口,重载基类方法,以日为周期进行调度-执行-休眠循环,苏醒后调度爬虫任务—按照调度计划处理任务(执行或等待)—计划完成后休眠至下一周期苏醒
    StopTask:任务终止方法,当前任务完成后终止任务计划,非强行中断任务子线程,若要强行中断,可直接结束主线程,任务子线程将自动中断

    2.1.3 规则读取层 RuleReader.py2.1.3.1 RuleReader
    __init__:初始化方法,确定规则读取模式(目前仅支持XML模式),模式有效时进行初始规则读取
    FreshRules:规则刷新方法,读取最新规则,默认以XML格式读取规则,若要采用其他方法(如数据库读取、Json接口读取等),可继承该基类后进行重载扩展

    2.1.3.2 GetRuleFromNode功能性函数,从XML节点中提取爬虫规则字典,属性及简单子节点直接提取入本级字典,复杂子节点递归提取为子级字典后加入本级字典。
    2.1.3.3 PrintNode调试用函数,用于打印XML节点数据,采用前序遍历法。
    2.1.3.4 SpiderRule爬虫规则字典(dict类型),存储爬虫规则参数,以福州旅游网爬虫规则为例:

    name:规则名称,通常以爬取来源站命名,如福州旅游网
    sourceId: 来源标识,参照文章来源枚举值,默认为0
    rule:子级明细规则字典
    url:来源Url,明细规则,如:http://lyj.fuzhou.gov.cn/lyjzwgk/lydt/index.htm
    reAbstract:文章摘要正则表达式,明细规则,扫描文章清单时使用,如:
    <li>.+?<span>\[(.+?)\].+?href="(.+?)".+?>(.+?)</a>.+?</li>
    reArticle:文章正文正则表达式,明细规则,扫描文章正文时使用,如:
    <div class="content-boxtext">(.+?)</div>\s*<div class="content-boxbottom">
    reImage:文章图片正则表达式,明细规则,扫描文章图片时使用,如:
    <IMG.+?src="(.+?)".+?/>
    module:爬虫模块名,明细规则,反射加载的爬虫模块名与类名,如FuZhouSpider
    regionId:目的地标识,明细规则,爬虫目的地对应的乐途目的地字典标识值,如130
    spiderName:爬虫编辑ID,明细规则,利用爬虫对内发布文章的虚拟编辑用户名,如jishutest
    isValid:有效标志,明细规则,启用该规则时为1,停用为0,禁用为-1
    minPage:最小页码,明细规则,分页爬取时第一页的页码参数,默认为-1(不分页)
    maxPage:最大页码,明细规则,分页爬取时最后页的页码参数,默认为-1(不分页)
    wakeTime:子级苏醒时间字典,明细规则,可包含多个时间点
    timePotX:时间点,X代表时间点标识,多个时间点标识不可相同,时、分、秒必须

    2.1.4 文章转存层 ArticleStorer.py2.1.4.1 ArticleStorer文章转存器,组织下层通信、数据模块进行数据交互,如数据库访问、队列消息发送、文件上传、远程接口调用等。

    __init__:初始化方法,设定图片转存API(imageApi)、数据库连接(dbConStr)、消息队列(msmqPath)及压图API(picCutApi)等通信参数,并进行初始化
    DumpImage:图片转存方法,转存文章正文中已缓存的的下载图片至图片转存API,同时关联转存图片路径
    DumpArticle:文章入库方法,将已经完成图片转存并替换Url的文章正文按照正文页入库,同时记录爬取关联信息
    NewArticleCheck:新文章检查方法,比对爬取关联信息,确定是否为新文章或可更新文章
    SendSuccessMessage:后期处理消息信息发送方法,向消息队列发送信息,通知后续处理程序处理刚刚发布或变更的新正文页
    CutImaages:正文页压图方法,调用压图接口对指定正文页正文内的图片进行压图处理
    ArticleRepublish:正文页重发布方法,将正文页重新发布,即向消息队连续发送一条删除发布请求与新发布请求

    2.1.5 文章爬虫层 Spider该层为目录,包含多个XSpider.py模块,其中Spider.py为基础模块,其他模块如FuZhouSpider.py为基于Spider.py模块的扩展的模板化模块。
    2.1.5.1 Spider爬虫基类,封装爬虫所需的基本操作,可由子类继承复用(如规则加载、HTML下载、图片下载等共同操作)或重载(文章抓取、图片替换等区别操作)。

    ReadRule:规则加载方法,可继承复用,可嵌套重载,加载爬虫规则字典内参数信息,并对爬虫伪装(Http Header)、数据缓存、数据清理(Css清理、Herf清理等)等参数进行设置
    CatchArticles:文章抓取方法,不可直接复用,必须替换重载,为各爬虫模板的独有逻辑,目前主要有页面抓取及异步JS抓取两种基本模式
    DownLoadHtml:Html源码下载方法,可继承复用,可重载(如进行拟人化访问伪装) ,用于获取抓取页面的相关Html源码,如文章列表页、文章正文页以及异步加载API
    DownLoadImage:图片下载方法,可继承复用,可重载(如应对图片防盗链机制),用于下载文章正文中的图片链接至缓存路径,同时进行图片格式标准化
    ReplaceArticleImages:缓存文章正文图片替换方法,可继承复用,可重载(如对转存图片标签添加属性),用于将抓取文章正文中原来的图片链接替换为转存后的图片链接
    CacheArticle:文章信息缓存方法,可继承复用,可重载(如定制文章特有属性信息),用于组装文章属性信息并加入缓存等待下一步处理
    ClearArticles:文章正文清洗方法,可继承复用,可重载(如添加JS清晰),用于清洗文章正文中的无效信息,如当前已支持的CSS、Herf、空白字符及样式清洗
    ClearTempImages:缓存图片清理方法,可继承复用,可重载(如添加缓存备份机制),用于清理缓存的已下载图片

    2.1.5.2 Functions文章爬虫层中的相关功能性函数。

    ReplaceImage:图像Url替换函数,可将文章正文中的正则匹配项(原始图片链接)替换为转存图片链接
    ClearExternalCss:CSS清理函数,可将文章正文中的正则匹配项(Css类)清空
    ClearExternalHerf:Herf清理函数,可将文章正文中的正则匹配项(超链接)去链接处理
    ClearExternalBlank:空白字符清理函数,可将文章正文中的正则匹配项(空白字符)去除
    ClearExternalStyle:样式清理函数,可将文章正文中的正则匹配项(style样式)去除
    ComposeUrl:相对Url组装函数,将页面中的相对Url结合页面Url组装为绝对Url
    ConvertImageToJpg:图片格式标准化函数,将不同格式的图片(如PNG、BMP)同意转化为JPG格式

    2.1.5.3 XSpider各种爬虫模板类,通常直接继承自Spider,但也可继承其他XSpider,区别主要在于CatchArticles方法的重载实现,目前主要分为两种模式。

    页面爬虫:CatchArticles方法直接解析页面源码,根据制定的编码格式,提取文章关键信息,由扫描列表页的文章摘要、扫描正文页的正文、扫描正文域的图片三步组成,典型模板如FuZhouSpider。
    Json Api爬虫:CatchArticles方法获取Json API响应,将结果反序列化为字典对象,提取文章关键信息,由提取列表API的文章摘要、提取正文API的正文与图片两步组成。,典型模板如FuZhouTourSpider。

    其他类型的模板可根据实际情况自行扩展,如XML API、SOAP API等。
    2.1.6 数据模型层 Model该层为目录,包含多个SpiderContext.py、SpiderEntity.py与SpiderData.py三个模块。

    SpiderContext.py:数据上下文模块,负责数据库连接、数据模型初始化、会话管理
    SpiderEntity.py:数据模型模块,负责数据实体对象模型映射,即表与类的映射
    SpiderData.py:数据逻辑模块,负责组织会话内的数据访问逻辑,也是对外接口

    2.1.6.1 SpiderDataHelper爬虫数据访问类,属于数据逻辑模块。

    SaveArticle:文章入库方法,将爬去并完成过滤的文章信息写入数据库,若是新文章,文章信息入库同时写入文章导入信息,若不是新文章(导入信息已存在),仅进行修改
    NewArticleCheck:新文章检查方法,用于防止文章重复导入,对比文章导入信息,若不匹配(根据文章Url与发布日期),则认定为新文章,否则认定为重复文章
    GetArticlesLikeDampSquibs:未成功发布文章扫面方法,用于查询出已发布但未成功的文章,返回必要属性信息,如正文页ID,目的地ID等

    2.1.6.2 ModelMapper实体关系映射(类表映射)函数,属于数据模型模块,将指定的实体类与数据表绑定,当前已映射对如下:

    Cms_Information—Cms_Information:正文页信息类与正文页信息表
    Cms_Information_Inported—Cms_Information_Inported:正文页导入信息类与正文页导入信息表
    Cms_InformationRegion—Cms_InformationRegion:正文页目的地关联类与正文页目的地关联表

    2.1.6.3 ModelContext数据模型上下文类,属于数据上下文模块,管理数据连接上下文。

    __init__:上下文初始化方法,注册数据库连接、初始化数据模型元数据并建立会话生成器
    Session:会话入口,申请一个数据库会话

    2.1.6.4 Session_Scope数据会话域函数,属于数据上下文模块,负责隔离会话事务的生命周期,具有contextmanager特性,即以迭代生成器的方式隔离会话事务逻辑无关的细节,确保成功自动提交、失败自动回滚并且结束自动释放。
    2.1.7 消息模型层 Message该层为目录,包含SpiderMessageQueue.py模块,负责格式化消息,并与消息队列通信。
    2.1.7.1 Message消息类,负责消息格式化,将消息转化为制定输出格式。

    __init__:初始化方法,将消息字典初始化为XML结构,同时添加必要属性
    ToFormatString:序列化方法,将XML结构的消息转化为字符串,同时添加必要说明性内容

    2.1.7.2 ToXmlElement功能性函数,将字典转化为XML结点格式,简单键值对直接转化为子节点,复杂键值对进行嵌套转化,值数组转化为多个子节点,值为字典则进行递归转化。
    2.1.7.3 MessageQueue消息队列访问类,封装消息队列接口。

    __init__:初始化方法,组装MSMQ队列基本信息,包含主机名与队列路径
    SendMessage:消息发送方法,根据给定MSMQ队列基本信息,创建会话并发送消息

    2.1.7.4 Queue_Scope队列会话域函数,负责隔离队列会话的生命周期,具有contextmanager特性,即以迭代生成器的方式隔离队列会话逻辑无关的细节,确保会话结束自动释放。
    3、部署说明3.1 运行环境
    PC配置

    2G以上内存
    操作系统

    Windows XP Professional 2002 Service Pack 3+
    相关工具

    Python 3.3 Pyodbc 3.0.7 SQLAlchemy 0.9.7pywin32 219requests 2.4.1Miscrosoft Message QueueMiscrosoft SQL Server 2005SQL Server Native Client 10.0

    3.2 资源目录
    源码路径,192.168.80.157主机 E:\shuaxin\ArticleSpider。
    工具路径:192.168.80.157主机 E:\tools\爬虫项目部署包

    3.3 部署步骤3.3.1 Python确认部署主机python-3.x版本已安装,建议使用python-3.3稳定版本。
    若未安装,执行爬虫项目部署包内python-3.3.5.1395976247.msi文件,以安装python虚拟机,安装目录可设置为E:\tools\Python33。
    确保系统环境路径(高级系统设置-环境变量)含有python安装目录,确保路径内可引用python.exe。
    3.3.2 MSMQ确保Miscrosoft Message Queue已开启,且存在消息队列路径\private$\queuepathnews,且该队列对EveryOne开放消息发送权限,若该队列不存在,则依赖部署条件不满足(后续处理程序未部署),不可进行应用部署。
    3.3.3 DataSource Driver确保主机ODBC数据源中已安装SQL Server Native Client 10.0驱动程序(版本可以更高,但ArticleStorer.py中dbConStr也应对应修改)已正确安装。
    若未安装,根据系统环境,选择执行爬虫项目部署包内sqlncli_X86.msi或sqlncli_X64.msi,以安装数据源驱动。
    3.3.4 Pyodbc确保python安装目录(如E:\tools\Python33)下,存在以下相对路径的目录Lib\site-packages\pyodbc…,即Pyodbc已安装。
    若未安装,根据系统环境,执行爬虫项目部署包内pyodbc-3.0.7.win32-py3.3.exe,以安装Python ODBC组件。
    3.3.5 Pywin32确保python安装目录(如E:\tools\Python33)下,存在以下相对路径的目录Lib\site-packages\pythonwin,即Pywin32已安装。
    若未安装,根据系统环境,执行爬虫项目部署包内pywin32-219.win32-py3.3.exe,以安装Python Win32组件。
    3.3.6 Pillow确保python安装目录(如E:\tools\Python33)下,存在以下相对路径的目录Lib\site-packages\PIL,即Pillow已安装。
    若未安装,根据系统环境,执行爬虫项目部署包内Pillow-2.6.0.win32-py3.3.exe,以安装Python Image Library组件。
    3.3.7 Requests确保python安装目录(如E:\tools\Python33)下,存在以下相对路径的目录Lib\site-packages\requests,即Requests已安装。
    若未安装,启动cmd,切换至爬虫项目部署包下requests-2.4.1目录,执行命令python setup.py install,以安装Python Requests组件。
    3.3.8 SQLAlchemy确保python安装目录(如E:\tools\Python33)下,存在以下相对路径的目录Lib\site-packages\sqlalchemy,即SQLAlchemy已安装。
    若未安装,启动cmd,切换至爬虫项目部署包下SQLAlchemy-0.9.7目录,执行命令python setup.py install,以安装Python SQLAlchemy组件。
    3.3.9 SQL Server确保81主机上的lotour库,已存在Cms_Information_Inported表。
    若不存在,执行初始化脚本:
    CREATE TABLE [dbo].[Cms_Information_Inported] ( [SourceUrl] VARCHAR (256) NOT NULL, [Status] SMALLINT NOT NULL, [InformationId] INT NOT NULL, [SourceTime] DATETIME NOT NULL, [RegionId] INT NOT NULL, [SourceName] VARCHAR(50) NULL, PRIMARY KEY CLUSTERED ([SourceUrl] ASC));CREATE NONCLUSTERED INDEX [IX_Cms_Information_Inported_InformationId]ON [dbo].[Cms_Information_Inported]([InformationId] ASC);
    3.3.10 Spider Service检查服务管理程序中,是否存在Article Spider Service服务,即爬虫服务是否已安装。若未安装,选定部署路径,将ArticleSpider目录移动至对应路径,启动cmd,切换至部署目录,执行命令python SpiderService.py install,以安装爬虫服务。
    应用扩展升级时无须重新安装,除非变更SpiderTask.py,亦无须停止Article Spider Service服务,直接添加或替换对应源文件即可(次日生效),但重启服务可确保变更立即生效。
    进入服务管理程序,启动Article Spider Service,若启动成功,整个部署流程完成。
    3.4 配置参数3.4.1 图片转存APIArticleStorer.py中的imageApi参数,指向CMS正文页的图片上传接口,默认为’http:// localhost:8037/WS/newsUploadImage.ashx’。
    3.4.2 数据库连接ArticleStorer.py中的dbConStr参数,对应Lotour库的数据连接字符串,默认为:
    mssql+pyodbc:// testUser:test@localhost:1433/news?driver=SQL Server Native Client 10.0其中mssql表示DBMS为Microsoft Sql Server,pyodbc表示驱动模式为Python ODBC,testUser为数据库用户名,test为用户密码,@至?间区域表示数据库路径,?后区域表示数据库驱动名称。
    3.4.3 消息队列ArticleStorer.py中的msmqPath参数,指向CMS正文页的发布消息队列,默认为:\PRIVATE$\queuepathnews。
    3.4.4 压图APIArticleStorer.py中的picCutApi参数,指向CMS正文页的压图接口,默认为: http://cms.lotour.com:8343/WS/newsCutImage.ashx 。
    3.4.5 图片缓存路径Spider.py中的temp参数,指向文章爬取过程中下载中间图片的缓存目录,默认为相对路径’\temp\‘。
    3.4.6 日志设置SpiderTask.py中的logging.basicConfig函数的相关入口参数,作为爬虫日志设置,可参考python官方文档中的logging部分,当前使用循环日志,部分参数如下:

    filename,日志文件名,默认使用相对路径’\Log\spider.log’
    mode,文件使用模式,默认使用添加模式,即’a’
    maxBytes,循环日志块大小,默认为2M
    backupCount,循环日志块数量,默认为8
    encoding,日志编码,默认为’utf-8’
    format,日志信息格式,用于将Trace信息与Message信息格式化,默认为:
    %(asctime)s %(levelname)-10s[%(filename)s:%(lineno)d(%(funcName)s)] %(message)slevel,日志记录级别,默认为DEBUG,线上部署推荐使用WARN或ERROR

    3.4.7 爬虫规则路径SpiderTask.py中的rulePath参数,默认为XML文件路径,默认值为相对路径’\SpiderRule.xml’。
    4、扩展说明4.1 扩展范围网络爬虫服务扩展分为三个级别,分别为规则扩展、模板扩展以及功能扩展,以应对不同的扩展需求。

    规则扩展:改动规则文件,即SpiderRule.xml,用于爬虫规则微调,以及添加仅需复用爬虫模板的新爬虫(如同站新频道,或同网站架构)
    模板扩展:新增爬虫实现,即添加XSpider.py,继承Spider基类,重载实现特有逻辑(如文章抓取逻辑),必要时可单独添加独立功能,如防盗链破解功能
    功能扩展:变更或重组爬虫功能,任何文件都可能改动,请在理解架构的前提下进行,如将规则元数据由XML元数据变更为可维护更大规模元数据的数据库元数据表,或者将爬虫服务重组以添加文章智能过滤层

    4.2 扩展示例4.2.1 规则扩展规则扩展仅需修改SpiderRule.xml文件,以福州旅游网爬虫为例:
    <rule name="福州旅游网" sourceId="0"><url>http://lyj.fuzhou.gov.cn/lyjzwgk/lydt/index.htm</url><reAbstract><li>.+?<span>\[(.+?)\].+?href="(.+?)".+?>(.+?)</a>.+?</li></reAbstract> <reArticle><div class="content-boxtext">(.+?)</div>\s*<div class="content-boxbottom"></reArticle> <reImage><IMG.+?src="(.+?)".+?/></reImage> <module>FuZhouSpider</module> <regionId>130</regionId> <spiderName></spiderName> <isValid>1</isValid> <minPage>-1</minPage> <maxPage>-1</maxPage> <wakeTime> <timePot0>08:00:00</timePot0> <timePot1>13:00:00</timePot1> </wakeTime></rule>
    一个Rule节点便是一项爬虫规则,结点属性及子节点含义参照3.1.3.4。
    每增加一个爬虫任务,则需要添加对应的Rule节点,添加流程如下:
    4.2.1.1 确定文章来源
    name:根据抓取需求,将name设置为[站名][-频道名]
    sourceId:进入CMS媒体管理模块,检查媒体来源,若不存在现有对应媒体来源,进行添加操作,将sourceId设置为对应媒体来源ID,若不指定媒体来源,忽略sourceId属性或置0
    url:观察抓取url对应源代码,若页面含有文章列表,即可按照网页抓取模式处理,url可直接设置为页面url;若列表内容为异步加载(可参考福州旅游资讯网),此时应分析源代码,找到对应的文章列表API,将url设置为对应API;若url中含有动态参数或分页参数,可使用{n}参数顺序替代(从{0}开始),在抓取逻辑中填入参数
    minPage & maxPage:若需求抓取页面不只一页,则将minPage置为起始页,maxPage置为终止页,同时定制扩展功能,利用参数化的url在抓取逻辑中进行处理
    regionId:抓取源应指定目的地,通常以乐途目的地字典中的ID值配置regionId即可,若有特殊情况(如混合目的地抓取),应在抓取逻辑中提取指定

    4.2.1.2 确定正则表达式
    reAbstract:若文章摘要为页面抓取模式,应分析页面源码,抽象出提取文章摘要信息的正则表达式,其中文章链接必须提取,文章标题与发布时间则是能提取尽量提取,将正则表达式进行html转义处理后置入reAbstract(参考在线转义工具如http://www.cnblogs.com/relucent/p/3314831.html )。若文章摘要为异步加载模式,reAbstract可按照抓取逻辑定制填写,正则表达式不必须,可空置或置为其他数据,如参数化文章正文API
    reArticle:若文章正文为页面抓取模式,应分析页面源码,抽象出提取文章正文信息的正则表达式,其中文章正文必须提取,文章标题与发布时间若在摘要中未提取也必须提取(无法提取时,应在抓取逻辑中另行处理)。若文章正文为异步加载模式,reArticle可按照抓取逻辑定制填写,正则表达式不必须,可空置或置为其他数据,如参数化图片API
    reImage:若图片隐藏在文章正文中,应分析正文代码,抽象出提取图片的正则表达式,图片链接必须提取。若图片信息独立于正文,reImage可按照抓取逻辑定制填写,正则表达式不必须,可空置或置为其他数据,如防盗链破解参数。

    4.2.1.3 确定爬虫模板module:不同规则的爬虫抓取逻辑可能各不相同,也可能存在复用,因此可在规则中指定爬虫模板,若无可复用模板,也可创建新爬虫模板,然后将module置为模板名称;模板名称格式通常为XSpider,不同模板名称不可重复,模板定义可参考模板扩展示例。
    4.2.1.4 确定任务计划
    isValid:爬虫规则可自由预设,但只有将isValid被置为1的规则,在爬虫任务计划扫描时进入任务队列,isValid置为0时表示停用,置为-1时表示禁用,其他取值可在功能扩展中确定
    spiderName:爬虫抓取文章后会发布正文页,默认会以匿名的乐途小编身份发表,此时spiderName置空,若要指定发布人,应将spiderName置为特定编辑者的CMS用户名
    wakeTime:爬虫任务若要执行,至少还需要一个执行时间,爬虫将在指定时间之后被唤醒,执行任务,此时应在wakeTime中添加timePotX子节点,X用于区别多个不同时间点(如0、1、2),意味着统一爬虫可在一日之内的不同时间启动多次

    4.2.2 模板扩展模板扩展需添加XSpider.py文件,在其中实现继承自Spider或其他模板Spider的XSpider类,其中CatchArticles必须重载以实现定制化的抓取逻辑,以福州福州旅游网爬虫为例:
    class FuZhouSpider(Spider.Spider): """福州旅游网 Spider""" def __init__(self): Spider.Spider.__init__(self) def CatchArticles(self): recAbstract = re.compile(self.reAbstract, re.DOTALL) recArticle = re.compile(self.reArticle, re.DOTALL) recImage = re.compile(self.reImage, re.DOTALL) html = self.DownLoadHtml(self.url, '文章列表页{0}访问失败,异常信息为:{1}') if html == None: return self.articles for x in recAbstract.findall(html): article = dict( time = datetime.datetime.strptime(x[0],'%Y-%m-%d'), # url = self.url[0:self.url.rfind('/')] + x[1][1:], url = Spider.ComposeUrl(self.url, x[1]), title = x[2] ) html = self.DownLoadHtml(article['url'], '文章页{0}访问失败,异常信息为:{1}') if html == None: continue content = None images = [] imageCount = 0 for y in recArticle.findall(html): content = y for z in recImage.findall(content): imageCount += 1 # imageUrl = article['url'][0:article['url'].rfind('/')] + z[1:] imageUrl = Spider.ComposeUrl(article['url'], z) image = self.DownLoadImage(imageUrl, '图片{0}提取失败,异常信息为:{1}') if image == None: continue images.append(image) if not content \ or imageCount != len(images): continue self.CacheArticle(article, content, images, '成功自{0}提取文章') return self.articles
    由于采用继承机制,__init__方法中应调用父级__init__方法,CatchArticles方法则可利用基类模板中的方法、字段及函数,结合自有方法、字段及函数扩展抓取逻辑,扩展流程如下:
    4.2.2.1 处理继承要素选定基类
    通常使用Spider.Spider,其中前一个Spider为模块名,对应Spider.py,后一个Spider为类名,如需更细粒度的模板复用,如定制页面抓取模板或异步抓取模板,可继承对应二级模板。
    选定重载方法
    继承模板通常会复用基类模板的大部分方法,但作为区别,必定有自身的特定逻辑,比如重载CatchArticles方法用以实现不同爬虫抓取逻辑(该方法在Spider中为虚方法,继承Spider后必须实现);
    除CatchArticles方法外,其他基类方法也存在扩展空间,可参考3.1.5.1中的方法说明进行重载。
    添加自有元素
    通常情况下,基类元素已经足够使用,但在一些特殊场景,可能需要添加一些新元素,比如文章过滤函数,图片抗反盗链下载之类的功能型函数,然后在重载的方法中使用。
    4.2.2.2 组织抓取逻辑抓取源加载
    根据爬虫规则中定义的url或者Api,从抓取源下载数据;
    页面抓取时,需指定指定抓取页面的编码格式,默认为utf-8编码,但部分页面会使用gbk或者gb2312编码;
    异步抓取或多页抓取时,API通常需要一些需要二次指定的参数,此时应将参数赋值再发起请求;
    抓取源通依照抓取模式存在区别,页面抓取源通常由三段式的文章列表页、文章正文页与文章正文组成,异步抓取源则随API的定义不同而不同,混合抓取则结合了前两者。
    基类抓取相关方法包括DownLoadHtml与DownLoadImage,DownLoadHtml用于加载Html源码或者Api响应数据,DownLoadImage则用于下载图片等文件至缓存路径。
    数据解析与提取
    抓取数据最终需转化为文章基本信息,用于后期处理,其中必要组成部分包含文章摘要信息(文章源链接、标题及发布日期等)、文章正文以及图片清单(图片链接及缓存路径等);
    页面抓取数据需要使用正则表达式解析,对应正则表达式由基类从爬虫规则读取,由reAbstract(文章摘要规则)、reArticle(文章正文规则)以及reImage(图片规则)组成,分别可提取信息至article(文章基本信息)、content(文章正文)以及images(图片列表);
    异步抓取通常不需要使用正则表达式,因为抓取数据通常为结构化数据,可以直接反序列化为友好的数据结构,如Json数据,可先进行清洗(将null替换为Python中的None),然后直接使用eval函数转化为数据字典;
    reAbstract、reArticle及reImage在异步抓取或混合抓取中的存储涵义开放,可配合模板自行定义,如API,筛选规则等,只需要保证最终可正确提取文章基本信息;
    提取文章发布时间时,应注意时间数据的表示形式,指定转化格式(如%Y-%m-%d可匹配2014-10-1),并注意从摘要数据和从正文数据提取的区别;
    正则解析函数主要使用findall进行全文搜索,注意该函数匹配结果为多项时将返回匹配结果迭代器,为单项时将直接返回匹配结果;
    信息提取完成后,应调用基类的CacheArticle方法,将article、content与images组装并缓存,等待后起批量处理。
    4.2.2.3 文章后期处理通常情况下图像后期处理对模板扩展透明,但其中的部分环节可进行独立定制,文章后期处理的流程为:
    有效性检查—图片转存—文章正文图片链接替换—正文清洗—文章入库—文章图片压图—发布。
    其中模板扩展可参与环节主要为文章正文图片链接替换(ReplaceArticleImages)与正文清洗(ClearArticles)环节。
    ReplaceArticleImages扩展示例如更改img标签的替换形式,如加入alt等属性。
    ClearArticles扩展示例如增加过滤规则,如JS过滤。
    4.2.3 功能扩展功能扩展自由度较大,但通常情况下是对爬虫服务任务流程进行重组。
    爬虫服务主流程为:
    扫描爬虫任务—执行爬虫任务—休眠,其功能扩展主要集中在任务管理层与规则读取层。
    特定规则的执行流程为:
    加载任务—文章抓取—有效性检查—图片转存—文章正文图片链接替换—正文清洗—文章入库—文章图片压图—发布,其功能扩展主要集中在文章转存层、数据模型层以及消息模型层。
    功能扩展的方式主要有两种,加入中间层以添加中间步骤以及重载替换特定步骤,对应的可扩展空间请参阅架构说明部分。
    1 留言 2019-05-02 11:18:20 奖励25点积分
  • 编程使用WMI 精华

    背景
    WMI出现至今已经二十多年了,但很多人对它并不熟悉。知道它很好很强大,但不知道它从哪里来,怎么工作,使用范围是什么?
      WMI有一组API。我们不管使用VBScript、PowerShell脚本还是利用C#的来访问WMI的类库,都是因为WMI向外暴露的一组API。这些API是在系统安装WMI模块的时候安装的,通过他们我们能够能拿到我们想要的类。
      WMI有一个存储库。尽管WMI的多数实例数据都不存储在WMI中,但是WMI确实有一个存储库,用来存放提供程序提供的类信息,或者称为类的蓝图或者Schema。
      WMI有一个Service。WMI总是能够响应用户的访问,那是因为它有一个一直运行的Windows服务,名字叫Winmgmt。停止这个服务,所有对WMI的操作都将没有反应。
      WMI是可扩展的。人人都知道WMI能干很多事情,读取本机硬盘信息、读取远程计算机的用户信息、读取域用户信息等等。基本上,你能想到的获取或者更改资源的操作,它都能干。可谓吃得少,干得多。它为什么这么能干呢?这基于WMI的可扩展性。WMI对资源的操作,不是它自己实现了什么方法,而完全取决于向它注册的提供程序。
      WMI是管理员日常必备的强大工具之一,是脚本伴侣。当然也可以把一个大型系统建立在WMI以及WMI的提供程序之上。
    WMI的全称是Windows Management Instrumentation,即Windows管理工具。它是Windows操作系统中管理数据和操作的基础模块。我们可以通过WMI脚本或者应用程序去管理本地或者远程计算机上的资源。对于VC和汇编程序员,想获取诸如CPU序列号和硬盘序列号等信息是非常容易的。但是对于VB以及其他一些脚本语言,想尝试获取系统中一些硬件信息可能就没那么容易了。微软为了能达到一种通用性目的(遵守某些行业标准),设计了WMI。它提供了一个通过操作系统、网络和企业环境去管理本地或远程计算机的统一接口集。应用程序和脚本语言使用这套接口集去完成任务,而不是直接通过Windows API。可能有人要问,为什么不让设计的脚本直接在底层使用Windows API,而非要弄个新的技术呢?原因是在目前Windows API中,有些是不支持远程调用或者脚本调用的。这样通过统一模型的WMI,像VB和脚本语言就可以去访问部分系统信息了。但是并不是所有脚本语言都可以使用WMI技术:它要支持ActiveX技术。

    WMI通常是被脚本所调用的,不过也对。对于WMI能做的操作,我们也完全可以通过VS去调用Win32 API去实现。但是,本文要讲的是使用VS调用WMI提供的接口去获取系统的信息。那么,VS中怎么使用WMI呢?接下来,我就把我所了解的知识分享给大家。
    函数介绍CoInitializeEx 函数
    为当前线程初始化COM库并设置并发模式 。
    函数声明
    HRESULT CoInitializeEx( void * pvReserved, DWORD dwCoInit);
    参数

    pvReserved系统 保留的参数,必须传入 NULL。dwCoInit该标示指明基于当前线程的并发模式和初始化选项。该参数是 COINIT 枚举类型,传入参数时候,除了COINIT_APARTMENTTHREADED 和COINIT_MULTITHREAD ED标记外,其余的标记可以组合使用。
    返回值

    S_OK :COM库初始化成功。S_FALSE :当前线程上,COM库已经被初始化。RPC_E_CHANGED_MODE :COM库已经被初始化且传入参数设置的并发模式和本次不同。

    CoInitializeSecurity 函数
    注册安全性并设置进程的默认安全性值。
    函数声明
    HRESULT CoInitializeSecurity( _In_opt_ PSECURITY_DESCRIPTOR pSecDesc, _In_ LONG cAuthSvc, _In_opt_ SOLE_AUTHENTICATION_SERVICE *asAuthSvc, _In_opt_ void *pReserved1, _In_ DWORD dwAuthnLevel, _In_ DWORD dwImpLevel, _In_opt_ void *pAuthList, _In_ DWORD dwCapabilities, _In_opt_ void *pReserved3);
    参数

    pSecDesc [in]服务器将用于接收呼叫的访问权限。cAuthSvc [in]asAuthSvc参数中的条目计数。只有当服务器调用CoInitializeSecurity时,此参数才被COM使用。如果此参数为0,则不会注册认证服务,并且服务器无法接收安全呼叫。值为-1表示COM选择要注册的身份验证服务,如果是这种情况,则asAuthSvc参数必须为NULL。但是,如果参数为-1,则服务器将不会选择Schannel作为身份验证服务。asAuthSvc [in]一组服务器愿意用来接收呼叫的认证服务。pReserved1 [in]此参数是保留的,必须为NULL。dwAuthnLevel [in]进程的默认身份验证级别。dwImpLevel [in]代理的默认模拟级别。此参数的值仅在进程为客户端时使用。pAuthList [in]指向SOLE_AUTHENTICATION_LIST的指针,它是一个SOLE_AUTHENTICATION_INFO结构的数组。dwCapabilities [in]通过设置一个或多个EOLE_AUTHENTICATION_CAPABILITIES值指定的客户端或服务器的附加功能。pReserved3 [in]此参数是保留的,必须为NULL。
    返回值

    S_OK :COM库初始化成功。RPC_E_TOO_LATE:CoInitializeSecurity 已经被调用。E_OUT_OF_MEMORY:内存不足。

    CoCreateInstance 函数
    用指定的类标识符创建一个COM对象,用指定的类标识符创建一个未初始化的对象。
    函数声明
    STDAPI CoCreateInstance( REFCLSID rclsid, //创建的Com对象的类标识符(CLSID) LPUNKNOWN pUnkOuter, //指向接口IUnknown的指针 DWORD dwClsContext, //运行可执行代码的上下文 REFIID riid, //创建的Com对象的接口标识符 LPVOID * ppv //用来接收指向Com对象接口地址的指针变量);
    参数

    rclsid[in] 用来唯一标识一个对象的CLSID(128位),需要用它来创建指定对象。pUnkOuter[in] 如果为NULL, 表明此对象不是聚合式对象一部分。如果不是NULL, 则指针指向一个聚合式对象的IUnknown接口。dwClsContext[in] 组件类别. 可使用CLSCTX枚举器中预定义的值。riid[in] 引用接口标识符,用来与对象通信。ppv[out] 用来接收指向接口地址的指针变量。如果函数调用成功,*ppv包括请求的接口指针。
    返回值

    S_OK:指定的Com对象实例被成功创建。REGDB_E_CLASSNOTREG:指定的类没有在注册表中注册. 也可能是指定的dwClsContext没有注册或注册表中的服务器类型损坏。CLASS_E_NOAGGREGATION:这个类不能创建为聚合型。E_NOINTERFACE:指定的类没有实现请求的接口, 或者是IUnknown接口没有暴露请求的接口。

    CoSetProxyBlanket 函数
    设置将用于在指定代理上进行呼叫的认证信息。
    函数声明
    HRESULT CoSetProxyBlanket( _In_ IUnknown *pProxy, _In_ DWORD dwAuthnSvc, _In_ DWORD dwAuthzSvc, _In_opt_ OLECHAR *pServerPrincName, _In_ DWORD dwAuthnLevel, _In_ DWORD dwImpLevel, _In_opt_ RPC_AUTH_IDENTITY_HANDLE pAuthInfo, _In_ DWORD dwCapabilities);
    参数

    pProxy [in]要设置的代理。dwAuthnSvc [in]要使用的身份验证服务。dwAuthzSvc [in]要使用的授权服务。pServerPrincName [in]要与身份验证服务一起使用的服务器主体名称。dwAuthnLevel [in]要使用的认证级别。dwImpLevel [in]要使用的模拟级别。pAuthInfo [in]指向建立客户端身份的RPC_AUTH_IDENTITY_HANDLE值的指针。由句柄引用的结构的格式取决于认证服务的提供者。dwCapabilities [in]这个代理的功能。
    返回值

    S_OK:执行成功。E_INVALIDARG:一个或者多个参数无效。

    实现原理现在,我们来解析下在VS2013中使用WMI的原理过程:

    首先,我们要使用CoInitializeEx初始化COM组件环境。因为WMI是基于COM组件实现的,使用COM组件前,必须要进行初始化操作。
    然后,使用CoInitializeSecurity注册安全性并设置进程的默认安全性值。
    接着,使用CoCreateInstance创建一个IWbemLocator的COM对象。
    跟着,通过IWbemLocator::ConnectServer函数连接到WMI,并获取IWbemServices指针。
    然后,使用CoSetProxyBlanket设置连接的安全级别。
    接着,使用IWbemServices指针发出WMI请求,执行WQL语句。
    然后,从查询返回集中获取获取返回数据。
    进行清理工作。

    其中,WQL其实非常简单,它有如下特点:
    SELECT 属性名 FROM 类名每个WQL语句必须以SELECT开始,SELECT后跟你需要查询的属性名,也可以像SQL一样,以*表示返回所有属性值。然后,FROM关键字,即你要查询的类的名字。
    本文使用的WQL语句是:SELECT * FROM Win32_Process。
    编码实现加载WMI所需的库文件#include <comdef.h>#include <WbemIdl.h>#pragma comment(lib, "wbemuuid.lib")
    调用WIM获取进程信息int WMI_EnumProcess(){ /*******************************************/ // 初始化操作 // /*******************************************/ // 初始化COM组件 HRESULT hRes = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); if (FAILED(hRes)) { printf("CoInitializeEx Error[%d]\n", hRes); return 1; } // 设置进程的安全级别 hRes = ::CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL); if (FAILED(hRes)) { printf("CoInitializeSecurity Error[%d]\n", hRes); return 2; } /*******************************************/ // 创建一个WMI命名空间连接 // /*******************************************/ // 创建一个CLSID_WbemLocation对象 IWbemLocator *pIWbemLocator = NULL; hRes = ::CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID *)(&pIWbemLocator)); if (FAILED(hRes)) { printf("CoCreateInstance Error[%d]\n", hRes); return 3; } // 使用 pIWbemLocator 连接到 "root\cimv2", 并获取 pIWbemServices IWbemServices *pIWbemServices = NULL; hRes = pIWbemLocator->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), NULL, NULL, NULL, 0, NULL, NULL, &pIWbemServices); if (FAILED(hRes)) { printf("pIWbemLocator->ConnectServer Error[%d]\n", hRes); return 4; } /*******************************************/ // 设置连接的安全级别 // /*******************************************/ // 设置连接的安全级别 hRes = ::CoSetProxyBlanket(pIWbemServices, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE); if (FAILED(hRes)) { printf("CoSetProxyBlanket Error[%d]\n", hRes); return 5; } /*******************************************/ // 执行WQL查询代码 // /*******************************************/ // 这里是列出正在运行进程的例子 // 为了接收结果, 你必须定义一个枚举对象 IEnumWbemClassObject *pIEnumWbemClassObject = NULL; hRes = pIWbemServices->ExecQuery(bstr_t("WQL"), bstr_t("SELECT * FROM Win32_Process"), // 类似数据库中的 SQL 语句 WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &pIEnumWbemClassObject); if (FAILED(hRes)) { printf("pIWbemServices->ExecQuery Error[%d]\n", hRes); return 6; } // 显示查询结果 IWbemClassObject *pIWbemClassObject = NULL; ULONG ulRet = 0; VARIANT varProp = { 0 }; while (pIEnumWbemClassObject) { // 获取下一个对象 hRes = pIEnumWbemClassObject->Next(WBEM_INFINITE, 1, &pIWbemClassObject, &ulRet); // 判断获取完毕结束 if (0 == ulRet) { break; } // 获取 ProcessId 字段的值 并 显示 ::VariantClear(&varProp); pIWbemClassObject->Get(L"ProcessId", 0, &varProp, NULL, NULL); printf("[%d]\t", varProp.intVal); // 获取 Name 字段的值 并 显示 ::VariantClear(&varProp); pIWbemClassObject->Get(L"Name", 0, &varProp, NULL, NULL); printf("[%ws]\n", varProp.bstrVal); } /*******************************************/ // 清理释放 // /*******************************************/ // 释放 ::VariantClear(&varProp); pIWbemServices->Release(); pIWbemLocator->Release(); ::CoUninitialize(); return 0;}
    程序测试在 main 函数中调用上述封装好的函数进行测试,main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ // 调用 WMI 去查询进程信息 WMI_EnumProcess(); system("pause"); return 0;}
    测试结果:
    运行程序,进程信息成功显示。所以测试成功。

    总结在VS中使用WMI,流程总是这么几步。要查询其他信息,也只是更改下WQL查询语句以及查询结果的显示而已。如果你学过数据库,或者了解过数据库相关的知识的话,你会发现WQL查询语句和数据SQL语句是很相似的。那是因为,我们之前提到的,WMI也有自己的数据库,也会在数据库里存储一些信息。
    参考文档WMI入门(一):什么是WMI
    WMI技术介绍和应用——WMI概述
    《Windows黑客编程技术详解》一书
    1 留言 2018-11-30 21:25:55 奖励30点积分
显示 30 到 45 ,共 15 条
eject