-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 403 KB
/
content.json
1
{"meta":{"title":"HPDell 的个人博客","subtitle":"我们在小孩和大人的转角盖一座城堡","description":"HPDell 的博客","author":"hpdell","url":"http://hpdell.github.io"},"pages":[{"title":"关于我","date":"2019-01-22T22:20:00.000Z","updated":"2022-04-14T16:50:55.585Z","comments":true,"path":"about/index.html","permalink":"http://hpdell.github.io/about/index.html","excerpt":"","text":"关于我其实没什么好说的。 欢迎在此留言。"},{"title":"分类","date":"2017-11-29T11:01:13.000Z","updated":"2022-04-14T16:50:55.645Z","comments":false,"path":"categories/index.html","permalink":"http://hpdell.github.io/categories/index.html","excerpt":"","text":""},{"title":"tags","date":"2017-12-04T22:29:30.000Z","updated":"2022-04-14T16:50:55.645Z","comments":false,"path":"tags/index.html","permalink":"http://hpdell.github.io/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"使用 CMake 统一管理并编译 C++/Python/R 算法包","slug":"cmake-cpp-pypi-cran","date":"2022-04-14T17:46:17.000Z","updated":"2022-04-14T16:50:55.409Z","comments":true,"path":"编程/cmake-cpp-pypi-cran/","link":"","permalink":"http://hpdell.github.io/编程/cmake-cpp-pypi-cran/","excerpt":"","text":"在数据分析领域,Python 和 R 都是比较常用的语言。这两种语言在使用上有很多的相似处,也有很多的不同。 一方面,这两个语言对于代码的执行效率都远远不如静态语言(如C++),尤其是循环的效率、矩阵运算的效率等。 另一方面,这两种语言使用起来都要更为方便,而且有许多其他的软件包可以使用,很容易就可以和其他算法一起使用,这点又是 C++ 这种静态语言不能比的。 所以长久以来形成了“用 Python 和 R 调用 C++ 计算”的模式,以发挥两类语言各自的特点。 Python 中可以使用 pybind 或者 Cython 调用 C++ 代码,而 R 可以用 Rcpp 调用 C++ 代码。 目前很多算法库都有 Python 和 R 版本,但是往往都是单独开发,甚至 Python 版本和 R 版本不是同一个作者。 为了解决这个问题,笔者尝试了使用 CMake 将算法计算部分的 C++ 代码和调用部分的 Python 与 R 代码统一管理,使开发者可以同时提供两种语言的版本。 项目结构本项目主要采用这样一种结构: / 根目录 CMakeLists.txt 主 CMake 配置文件 include C++ 头文件 src C++ 源文件 test C++ 单元测试 python Python 模块代码 mypypackage Python 代码,主要包含用于调用 C++ 的 Cython 代码 test CMakeLists.txt Python 模块的 CMake 配置文件 setup.py 用于构建和发布的 scikit-build 脚本 R R 包代码 data 用于存放包提供的数据文件 man 用于存放其他文档 R 用于调用的 R 代码 src 用于调用库的 C++ 代码 CMakeLists.txt R 包的 CMake 配置文件 DESCRIPTION.in R 包 DESCRIPTION 模板,在 CMake 项目配置时自动填入版本号等信息 NAMESPACE.in R 包 NAMESPACE 模板,在 CMake 项目配置时自动填入版本号等信息 根目录中可以添加一些持续集成配置文件、文档源文件等其他文件。 总体上,该项目结构是一个 C++ 项目的格式,在开发时也是先开发 C++ 代码,在 C++ 代码的基础上再开发 Python 或 R 代码,甚至其他语言的代码。 设计思路在这个包中,根目录中的 C++ 代码主要负责实现算法内核的部分,即与所有调用语言无关的东西。在这里面,不能使用 Python 中的 DataFrame 或者 R 中的任何类型,只能使用纯 C++ 支持的类型。也就是说,需要调用者在 C++ 程序中可以直接调用这个库。这个算法核心部分通过 /test 目录下的代码进行单元测试,只要测试通过就说明算法核心没有问题。 目录 Python 和 R 中的代码主要是提供这些语言对于调用 C++ 代码的支持。一般情况下,都是这样一个顺序:Python 或 R 函数调用中间件、中间件调用 C++ 库。所以这两个目录中就需要包含 Python 或 R 函数(简称包函数)以及中间件这两个部分。在 Python 中,中间件往往是用 Cython 或者 Pybind 编写的,为包函数提供了 Python 对象和 C++ 对象进行对接的能力;在 R 中,中间件往往是用 C++ 编写的,依靠 Rcpp 包提供的能力,将 R 语言对象转换为 C++ 对象,并调用 C++ 库函数。 具体实现为了描述方便,我们将 CMakeLists.txt 文件统称为配置文件。 根配置文件的编写比较简单,主要就是设置一些变量,例如是否有 Python 模块的 WITH_PYTHON 等,然后添加一些目录。下面是一个示例 12345678910111213141516171819202122232425# /CMakeLists.txtcmake_minimum_required(VERSION 3.12.0)project(myproject VERSION 0.1.0)set(CMAKE_CXX_STANDARD 11)set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)option(WITH_R \"Whether to build R extension\" OFF)set(TEST_DATA_DIR ${CMAKE_SOURCE_DIR}/test/data)add_subdirectory(src)include(CTest)enable_testing()add_subdirectory(test)set(CPACK_PROJECT_NAME ${PROJECT_NAME})set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})include(CPack)if(WITH_R) add_subdirectory(R)endif() 主要工作就是向根配置文件中,引入 C++ 配置文件和测试用例,以及当 WITH_R=ON 时引入 R 配置文件。 C++ 配置文件这部分的配置文件写法和一般 CMake 管理的 C++ 库没有什么区别,所做的就是查找一些库、构建库或可执行程序。 可以无需考虑 Python 或 R 的部分。 R 配置文件R 配置文件相对比较简单一些。由于 R 有自己的包结构,如果要发布到 CRAN 中的话,就需要按照这种结构来提交。 而且 R 包的安装是通过 R CMD INSTALL 命令进行安装的,不太适合用文件拷贝的方式进行安装。 那么我们可以根据现有的代码结构,单独使用一个文件夹,程序化构造 R 包的结构,并调用 R 相关命令进行包的构建。 于是我们可以充分利用 CMake 提供的文件操作命令以及 add_custom_target() 方法实现这一目的。 在 CMake 配置和生成阶段,我们可以使用以下命令来生成一个 R 包的标准结构。 1234567891011121314# /R/CMakeLists.txtset(PROJECT_RBUILD_DIR ${CMAKE_BINARY_DIR}/${PROJECT_NAME})make_directory(${PROJECT_RBUILD_DIR})configure_file(DESCRIPTION.in ${PROJECT_RBUILD_DIR}/DESCRIPTION)configure_file(NAMESPACE.in ${PROJECT_RBUILD_DIR}/NAMESPACE)file(COPY cleanup DESTINATION ${PROJECT_RBUILD_DIR})file(COPY configure DESTINATION ${PROJECT_RBUILD_DIR})file(COPY configure.ac DESTINATION ${PROJECT_RBUILD_DIR})file(COPY ${CMAKE_SOURCE_DIR}/R/R DESTINATION ${PROJECT_RBUILD_DIR})file(COPY ${CMAKE_SOURCE_DIR}/R/src DESTINATION ${PROJECT_RBUILD_DIR})file(COPY ${CMAKE_SOURCE_DIR}/R/man DESTINATION ${PROJECT_RBUILD_DIR})file(COPY ${CMAKE_SOURCE_DIR}/R/data DESTINATION ${PROJECT_RBUILD_DIR})file(COPY ${CMAKE_SOURCE_DIR}/include/header.h DESTINATION ${PROJECT_RBUILD_DIR}/src)file(COPY ${CMAKE_SOURCE_DIR}/src/sources.cpp DESTINATION ${PROJECT_RBUILD_DIR}/src) 这样在 CMake 构建目录下,会出现一个 ${PROJECT_RBUILD_DIR} 的目录,里面就是一个标准结构的 R 包。 接下来,所有与 R 相关的操作,就都可以针对这个文件夹进行。下面是一个对 R 包进行编译、生成文档、打包的示例。 1234567891011# /R/CMakeLists.txtadd_custom_target(mypackage_rbuild VERBATIM WORKING_DIRECTORY ${PROJECT_RBUILD_DIR}/.. COMMAND_EXPAND_LISTS COMMAND ${CMAKE_COMMAND} -E make_directory \"${PROJECT_NAME}.library\" COMMAND ${R_EXECUTABLE} CMD INSTALL --preclean --clean --library=${PROJECT_NAME}.library ${PROJECT_NAME} COMMAND ${R_EXECUTABLE} -e \"roxygen2::roxygenize('${PROJECT_NAME}', load_code = 'source')\" COMMAND ${R_EXECUTABLE} CMD build ${PROJECT_NAME}) 基于这种思路,我们同样可以编写一个测试,就用来执行 R CMD check 命令。 123456# /R/CMakeLists.txtadd_test( NAME Test_R_mypackage COMMAND ${R_EXECUTABLE} CMD check ${PROJECT_NAME}_${PROJECT_VERSION_R}.tar.gz --as-cran --no-manual WORKING_DIRECTORY ${PROJECT_RBUILD_DIR}/..) 此外,如果使用 VSCode 进行开发,且使用 CMake 作为配置提供工具, 使用这种方式会导致自动类型提示无法在 RcppExports.cpp 等 R 包所需的 C++ 文件中工作。 解决方法也非常简单,就是写一个正常的 CMake 生成目标即可,但是将这个生成目标排除在 ALL 目标之外。 1234567# /R/CMakeLists.txtfind_package(R REQUIRED)include_directories(${R_INCLUDE_DIRS} ${RCPP_ARMADILLO_INCLUDE_DIR} ${RCPP_INCLUDE_DIR})include_directories(../include)add_library(mypackage_rcpp_export SHARED src/RcppExports.cpp)target_link_libraries(mypackage_rcpp_export mylib)set_target_properties(mypackage_rcpp_export PROPERTIES EXCLUDE_FROM_ALL TRUE) 这样,我们就可以完全依靠 CMake 的指令操作所有的流程。例如 1234mkdir build && cd buildcmake .. -DWITH_R=ONcmake --build . --config Release --target mypackage_rbuildctest -R Test_R_mypackage --output-on-failure 在持续集成中也可以采用这样的操作,避免因为 R 解释器以及操作系统的问题,造成很多不必要的麻烦。 Python 配置文件Python 的配置文件会相对来说更复杂一点,因为涉及到 Cython 语言的编译问题。 好在 scikit-build 库已经提供了使用 CMake 编译 Cython 文件的方法,而且有打包的功能。 那么我们可以直接使用 scikit-build 编译,也可以像 R 包一样,构建一个 scikit-build 所需要的结构, 并将这个包作为提交 Pypi 的包。 使用 CMake 直接编译根据 scikit-build 的文档,我们可以用这样的配置直接编译一个 Python 模块(pyd 文件) 12345# /python/mypackage/CMakeLists.txtadd_cython_target(pymypackage.pyx CXX)add_library(pymypackage MODULE ${pymypackage})target_link_libraries(pymypackage mylib ${ARMADILLO_LIBRARIES} ${Python3_LIBRARIES} Python3::NumPy)python_extension_module(pymypackage) 然后在 Python 模块代码的配置文件中引入即可 1234# /python/CMakeLists.txtinclude_directories(${MYLIB_INCLUDE_DIR})add_subdirectory(\"mypypackage\")add_subdirectory(\"test\") 编译好的 Python 模块就可以直接通过 import 关键字引入。 同样我们可以写一些 install 脚本,这样就可以直接将编译好的包安装在本地。 使用 Scikit-Build 编译与 R 包类似,构建 Pypi 包无非就是拷贝一些文件到一个目录,形成对应的结构。例如我们可以这样 1234567891011121314151617# /python/CMakeLists.txtset(PYMYPACKAGE_SKBUILD_DIR ${CMAKE_BINARY_DIR}/pymypackage)add_custom_target(pymypackage_skbuild VERBATIM COMMAND_EXPAND_LISTS COMMAND ${CMAKE_COMMAND} -E make_directory ${PYMYPACKAGE_SKBUILD_DIR} COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/python ${PYMYPACKAGE_SKBUILD_DIR} COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/cmake ${PYMYPACKAGE_SKBUILD_DIR}/cmake COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/include ${PYMYPACKAGE_SKBUILD_DIR}/include COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/src ${PYMYPACKAGE_SKBUILD_DIR}/src) 包结构设置好了,下面就是使用 Scikit-Build 进行编译。 虽然 Scikit-Build 最终还是通过 CMake 构建的,但是这种方式支持 python 命令编译安装。 我们需要编写 setup.py 和 pyproject.toml 文件。 123456789# setup.pyfrom skbuild import setupsetup( name=\"pymypackage\", version=\"0.2.0\", author=\"myname\", packages=[\"pymypackage\"], install_requires=['cython']) 12345678910111213[build-system]requires = [ \"setuptools>=42\", \"wheel\", \"scikit-build>=0.12\", \"cmake>=3.18\", \"ninja\", \"cython\", \"numpy\", \"pandas\", \"geopandas\"]build-backend = \"setuptools.build_meta\" 此时如果使用 python setup.py 的方式安装,到此为止只是告诉 Python 要使用 Scikit-Build 安装,以及如何使用这个工具安装。 但是还没有告诉 Scikit-Build 怎么去安装。 这一步还是通过写 CMake 配置文件进行实现的,通过这个配置文件就告诉 Scikit-Build 使用什么样的步骤构建并编译包。 12345678910111213# /python/CMakeLists.txtset(CMAKE_CXX_STANDARD 11)set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)find_package(Armadillo REQUIRED)if(MSVC) add_definitions(-D_CRT_SECURE_NO_WARNINGS)endif(MSVC)set(MYLIB_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include)include_directories(${MYLIB_INCLUDE_DIR})add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src ${pygwmodel_BINARY_DIR}/mylib)add_subdirectory(\"pygwmodel\")enable_testing()add_subdirectory(\"test\") 那么问题来了,这段 CMake 配置应该写在哪里呢? 应该是 /python/CMakeLists.txt ,因为这个文件是我们 Pypi 包结构的根配置文件。 但是构建这个包的 CMake 配置写在哪里呢? 还是这个文件,因为 /python 目录是 Python 包代码的根目录。 这样就产生了一个冲突,这个文件该如何包含两种配置? 为了解决这个问题,我们可以使用 Scikit-Build 提供的一个 CMake 宏 SKBUILD 。 如果定义了这个宏,那就说明是 Scikit-Build 在使用这个配置文件; 如果没有,那就说明不是 Scikit-Build 在使用。 但是如果不是 Scikit-Build 在使用,我们依然也需要分成两种情况:直接用 CMake 编译和构建 Pypi 包。 因此我们需要再定义一个 USE_SKBUILD 的宏,来区分这两种情况。 综合起来,配置文件 /python/CMakeLists.txt 就需要写成下面这个形式 123456789101112131415161718192021222324252627282930313233343536# /python/CMakeLists.txtif(SKBUILD) set(CMAKE_CXX_STANDARD 11) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) find_package(Armadillo REQUIRED) if(MSVC) add_definitions(-D_CRT_SECURE_NO_WARNINGS) endif(MSVC) set(MYLIB_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include) include_directories(${MYLIB_INCLUDE_DIR}) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src ${pygwmodel_BINARY_DIR}/mylib) add_subdirectory(\"pygwmodel\") enable_testing() add_subdirectory(\"test\")elseif(USE_SKBUILD) set(PYMYPACKAGE_SKBUILD_DIR ${CMAKE_BINARY_DIR}/pymypackage) add_custom_target(pymypackage_skbuild VERBATIM COMMAND_EXPAND_LISTS COMMAND ${CMAKE_COMMAND} -E make_directory ${PYMYPACKAGE_SKBUILD_DIR} COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/python ${PYMYPACKAGE_SKBUILD_DIR} COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/cmake ${PYMYPACKAGE_SKBUILD_DIR}/cmake COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/include ${PYMYPACKAGE_SKBUILD_DIR}/include COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/src ${PYMYPACKAGE_SKBUILD_DIR}/src )else() include_directories(${MYLIB_INCLUDE_DIR}) add_subdirectory(\"mypypackage\") add_subdirectory(\"test\")endif() 这样,就可以用这一个配置文件,实现三种不同的构建。 如果只需要本地构建,配置 CMake 的时候带上 -DWITH_PYTHON=ON 参数; 如果需要构建包结构,带上 -DWITH_PYTHON=ON -DUSE_SKBUILD=ON 参数, 然后使用 Python 解释器运行 setup.py 脚本就可以构建了。 参考仓库关于 Python 部分,可以参考仓库 hpdell/libgwmodel,该仓库是按照本文所描述的方式编写的。 关于 R 部分,上述仓库中虽然有,但是方法比较陈旧了,与本文描述也有一定的出入。 使用本文方法编写的仓库暂时还不适合开源,但会尽快开源。 届时将补充在本文中。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"R","slug":"R","permalink":"http://hpdell.github.io/tags/R/"},{"name":"C++","slug":"C","permalink":"http://hpdell.github.io/tags/C/"},{"name":"CMake","slug":"CMake","permalink":"http://hpdell.github.io/tags/CMake/"},{"name":"Python","slug":"Python","permalink":"http://hpdell.github.io/tags/Python/"},{"name":"Cython","slug":"Cython","permalink":"http://hpdell.github.io/tags/Cython/"}]},{"title":"原神中为什么暴击暴伤比为1:2最好?","slug":"genshin-impact-critical","date":"2022-03-25T14:42:00.000Z","updated":"2022-04-14T16:50:55.449Z","comments":true,"path":"理论/genshin-impact-critical/","link":"","permalink":"http://hpdell.github.io/理论/genshin-impact-critical/","excerpt":"","text":"原神中的圣遗物属性搭配是一个既烧摩拉又有趣的问题。相信很多玩家听说过一个结论,“角色面板暴击暴伤比为1:2时,期望伤害最高”。那为什么会得到这个结论呢?这涉及到很多有趣的数学问题,这篇博客中我们尝试进行推导。但是,由于实际角色伤害还取决于攻击力,但是引入攻击力的会使问题变得很复杂,这里就认为攻击力是不变的。确切地说,是在更换不同圣遗物或武器后,角色攻击力保持不变。 伤害的数学期望暴击率、暴击伤害,是动作游戏中很经典的两个属性。暴击率,就是指角色在一次攻击时发生暴击的概率。在原神中,假设一名角色的暴击率是 $p$,暴击伤害是 $q$,在不暴击的时候,伤害总值是 $a$,那么在攻击发生暴击时,当次攻击的伤害变为 $(1+q)a$。那么,对于攻击造成伤害这一事件,其取值 $A$ 服从如下分布列: $1-p$ $p$ $A$ $a$ $(1+q)a$ 那么一次伤害的数学期望值 $e$ 就有 $$ \\begin{aligned} e = E(A) &= (1-p)a+p(1+q)a \\\\ &= a + pqa \\\\ &= (1+pq)a \\end{aligned} $$ 在我们当前的假设中,可以认为 $a$ 是一个常数,$p$ 和 $q$ 是两个变量。那么我们分解将 $e$ 对 $p,q$ 求偏导, $$ \\frac{\\partial e}{\\partial p} = qa, \\quad \\frac{\\partial e}{\\partial q} = pa $$ 由于 $a,p,q$ 都是正数,所以理论上,暴击率越高,暴击伤害越高,伤害期望也越高。但是这似乎与之前的结论矛盾。为什么会出现这个情况呢? 伤害期望的约束最大值事实上,这是由于原神中“圣遗物”系统和“武器”系统的特性决定的。上述结论没有考虑到暴击率和暴击伤害取值的约束条件。下面先简述一下圣遗物和武器系统,来找到问题中的约束条件。 圣遗物和武器系统简介原神中每个人物最初始的情况下,有 5% 的暴击率和 50% 的暴击伤害。出了每个人物突破会提升暴击率暴击伤害以外,这个数值可以通过装备不同的圣遗物和武器进行提升。原神中每个人物最多装备5件圣遗物和1把武器,每件圣遗物(按五星计算)有1个主属性和4个副属性,每把武器有一个主属性和一个副属性。对于圣遗物而言,有以下几条约束: 圣遗物中只有“理之冠”(俗称“头”)有可能是暴击率或暴击伤害;每件圣遗物的副属性都有可能是暴击率和暴击伤害。但是所有主属性和副属性都不可能有重复的属性。 每件圣遗物最多强化到20级,每次升级都会强化主属性,但每提升4级才会提升一次副属性(如果副属性不足4个则有限生成一个副属性)。 每次强化虽然会强化到不同的属性,但是数值的比例是大概相同的,以使属性“总量”不会发生太大的变化。也就是说,不会出现一次强化出现100%暴击率的情况。 每次提升属性时,提升到暴击率和暴击伤害的数值大约是 $1:2$ 的比例,也就是说,在一次提升时,如果提升的是暴击率且提升了3.8%,那么如果这次提升发生在暴击伤害上,则提升幅度大约是7.6%。 对于武器而言,相对来说就简单很多。每把武器的主属性必然是攻击力,副属性虽然可能是暴击率或暴击伤害,但都是确定的。级如果只看副属性是暴击率或暴击伤害的武器(俗称“暴击武器”或“暴伤武器”),平均每次升级时暴击率和暴击伤害提高的数值差不多也是 $1:2$ 的比例。 在这样的情况下,我们可以假设一个暴击率和暴击伤害的总量 $c$ ,这个量和暴击率与暴击伤害的关系就是 $$ c=2p+q $$ 这将成为我们计算期望伤害最大值的约束条件。但事实上,我们不可能保证两件圣遗物的暴击暴伤总量完全相同,这里只是做一个理论计算,分析暴击率和暴击伤害的最佳配比。 暴击暴伤总量不变情况下的伤害期望最大值根据之前推导的伤害期望 $e$ 与暴击暴伤的函数关系,和暴击暴伤总量 $c$ 与暴击爆伤函数关系的约束条件,有 $$ \\begin{aligned} e &= (1+pq)a \\\\ c &= 2p + q \\end{aligned} $$ 要求 $e$ 的约束最大值,可以使用拉格朗日乘数法,构造 $\\phi=2p+q-c$ ,$F=e+\\lambda\\phi$,可以得到下面的方程组 $$ \\begin{aligned} \\frac{\\partial F}{\\partial p} & = qa+2\\lambda = 0 \\\\ \\frac{\\partial F}{\\partial q} & = pa+\\lambda = 0 \\\\ \\frac{\\partial F}{\\partial \\lambda} &= 2p+q -c = 0 \\end{aligned} $$ 通过解上述方程组可以很容易得到,当 $q=2p=\\frac{1}{2}c$ 的时候,函数 $e$ 取得极值,此时 $\\lambda=-\\frac{1}{4}ca$。而由于极值点只有这么一个,所以这个就是最值点。那么这个是最大值还是最小值呢? 一种方法是,由于一般 $p \\in [0.05,1]$ 而 $q\\in[0.5, q_m]$,这里的 $q_m$ 是所有装备暴伤拉满时的数值。这个数值虽然我没有见过,但是肯定是存在而且确定的。虽然不知道,但没关系,因为当 $p=0.05$ 时,$q=c-0.1$,此时 $pq=0.05c-0.005$;而当 $p=\\frac{1}{4}c$ 时,$pq=\\frac{1}{8}c^2$。令 $$ g=\\frac{1}{8}c^2-\\frac{1}{20}c+\\frac{1}{200} = \\frac{1}{8}\\left(c-\\frac{1}{5}\\right)^2 $$ 显然,$g \\geqslant 0$ 恒成立,当且仅当 $c=0.2$ 时 $g=0$。而人物的原始面板就已经保证了 $c=0.6$ ,搭配装备后只会使得 $c\\geqslant 0.6$,所以 $\\frac{1}{8}c^2 \\geqslant 0.05c-0.005$ 。所以,当 $q=2p=\\frac{1}{2}c$ 的时候,函数 $e$ 取得极大值。 另一种方法是,画个图看一下。用 Geogebra 画出这几个(隐)函数的图像,再把这个极值点标出来,就知道时最大值还是最小值了。 图中蓝色的绿色的曲面代表了函数 $e$ ,蓝色的平面代表了约束条件,红色的曲线就是满足约束条件时 $e$ 的可能取值,黑色的点就是极值点。显然,这是一个最大值点。 由此,我们最终得到结论:在暴击暴伤总量 $c$ 不变的情况下,当暴击率与暴击伤害比值为 $1:2$ 的时候,伤害期望是最高的。 暴击头还是爆伤头当角色有暴击武器或者暴伤武器的时候,这不是问题。由于属性稀释,直接选择另一个属性的理之冠即可。但是当角色没有暴伤武器或者暴击武器的时候,例如迪卢克和狼的末路,那么理之冠是选择暴击头还是爆伤头呢? 我们先假设其他圣遗物副属性完全没有暴击暴伤的情况。 当选择暴击头时,$p=0.361$ ,$q=0.5$ ,$pq=0.1802$ 。 当选择暴伤头时,$p=0.05$ ,$q=1.122$ , $pq=0.0561$ 。 这样就一目了然了,优先选择暴击头。那么这是什么原因呢? 一种简单的理解是属性稀释。确实,暴伤也是会被稀释的。准确地说所有属性都会被稀释,而当属性提升前的值 $x_0$ 越高时,提升量 $x$ 的稀释越严重。因为提升度 $t$ 有 $$ t = \\frac{x}{x_0+x}, \\frac{\\partial t}{\\partial x_0} = -\\frac{x}{(x_0-x)^2} $$ 所以 $t$ 随 $x_0$ 的增大而减小。暴伤基础值是 0.5,暴击基础值是 0.05 ,所以装备暴击头时提升幅度有8倍,但是带暴伤头时提升幅度只有2倍,所以最好带暴击头。 另一种理解,我们其实可以算出来消灭一只怪物所需要的平均攻击次数(这里简化一下)。因为怪物的血量 $m$ 是一定的,所需要的攻击次数 $n$ 就应该有 $$ \\begin{aligned} n&=\\frac{m}{e}=\\frac{m}{(1+p(c-2p))a} \\\\ \\frac{\\partial n}{\\partial p}&=-\\frac{m}{e^2}(c-4p)a \\end{aligned} $$ 当 $p<\\frac{1}{4}c$ 时,$n$ 递增,当 $p<\\frac{1}{4}c$ 时,$n$ 递减。而只有理之冠的主属性有暴击暴伤时, $c$ 基本上是 1.222,所以其实 $p=0.3055$ 时,需要的攻击次数是最低的。而且,由于 $n$ 在大于0的部分是对称的,对称轴是 $\\frac{1}{4}c$ ,所以装暴击头时 $p$ 的值离对称轴更近,所以总攻击次数更小。 对于迪卢克这种突破加暴击的角色,满级时暴击率是 0.242,暴击暴伤总量 $c$ 达到 1.606 。装备暴击头时,$p=0.553$ ,与 $c/4$ 差距是 0.1515;装备暴伤头时,暴击率与 $c/4$ 差距是 0.1595。所以还是装备暴击头更好,当然差距不大,完全可以看副属性那个好带那个。 此外还有一点,迪卢克是一个爆发不是很离谱的角色。对于优菈这种爆发离谱的角色,暴击率带高一些可以少凹几次,减少痛苦。 当然对于胡桃这样,突破加暴伤,暴伤专武,就不用纠结了,最终还是配平暴击暴伤比,尽量 $p=2q$,并且让 $c$ 越高越好。","categories":[{"name":"理论","slug":"理论","permalink":"http://hpdell.github.io/categories/理论/"}],"tags":[{"name":"原神","slug":"原神","permalink":"http://hpdell.github.io/tags/原神/"}]},{"title":"自己动手写动态博客(二)","slug":"dynamic-blog-v2","date":"2022-03-20T20:34:46.000Z","updated":"2022-04-14T16:50:55.429Z","comments":true,"path":"编程/dynamic-blog-v2/","link":"","permalink":"http://hpdell.github.io/编程/dynamic-blog-v2/","excerpt":"","text":"缘起2019年,我写了一个动态博客,并发表了一篇博客《自己动手写动态博客》,主要介绍了基于 Quasar 和 Express 的动态博客框架。但是后面这个动态博客被废弃了,原因是多方面的。一方面项目的部署没有基于 Docker,导致维护起来比较复杂。另一方面,该项目是前后端分离的,文章内容通过接口进行获取,再加上没有 SSR,所以就算想要提交搜索引擎,也很难进行搜索。此外,项目缺乏一个高可用性的后端管理平台。再加上当时网页设计水平有限,很多涉及其实比较反人类,因此最终还是继续使用了基于 GitHub Pages 的静态博客。 其实在做这个动态博客之前,并没有真的打算做一个博客出来,只是为了试一下 django-vditor 这个包,甚至项目文件夹名称都是 test-django-vditor,因为 Vditor 差不多已经是前端最强 Markdown 编辑器了。倒是初衷和博客有一定关系。2020年初设计了 GWmodel Lab 的主页,内容很全。由于网页是部署在群晖的 Web 服务器上,限制很大,所以项目虽然是前后端分离,但是总体上是一个静态网页,所有的 API 全都保存成了 JSON 文件。但是维护起来非常麻烦。这个主页一共分为了三个仓库:React 前端、Markdown 内容、Django 管理平台。当需要增加内容时,先在写好 Markdown 内容,使用 Git 进行版本管理,然后在 Django 中向数据库中添加相应的项目,再将接口响应内容保存成 JSON 文件,最后将 React 前端和内容以及接口相应内容部署到 Web 服务器上。当我还在实验室的时候,整个流程已经被打通,更新还算方便。但是交接给师弟的时候,想要讲清楚整个流程,就非常难了。 那么有没有一种比较方便的管理方法呢?有,其实只要找到一种方法,让 Django 直接可以输出整个网页,然后使用一些脚本将这些网页保存成 HTML 文件,再发布到网页服务器上就可以了。这个过程甚至可以让 Django 自己完成。配合 Django 的管理平台,更新内容就不复杂了。进一步地,如果要对搜索引擎友好,可以抛弃前后端分离的思路,反而使用 Django 的模板引擎渲染页面。 基于以上思路,我做了这个动态博客,就当作实验室主页的一个小 Demo。 框架这个动态博客的框架非常简单,就是 Django + Bootstrap,没有 MVVM 框架,没有前后端分离,以至于写的时候仿佛回到了自己 2016 年用 Bootstrap 写网页的时代。 但是这个框架对于个人博客来讲,就显得很方便了。毕竟个人博客的项目复杂度不高,没有使用前端框架的必要,也没有前后端分离的必要,事实上,前后端分离反而会带来很多其他麻烦。而且 Django 现在的功能已经非常强大了,很多功能(例如图片上传、权限管理)只需要增加一些配置就可以解决,其管理平台更是可以让我们少些很多代码。经过几年的发展,Bootstrap 使用起来也很方便了,而且样式并不过时,也很简洁,很适合个人博客。 下面介绍一些具体的细节。 数据库似乎每一个教关系数据库的教程,都会用博客作为案例场景进行介绍。这是因为这个场景非常简单,但是涉及了一对多、多对多两种关系。通常,会有四张表:文章(Post)、分类(Category)、标签(Tag)和作者(Author)。本框架暂时省略了作者表,因为目前来说作者只有我一个人。数据库中主要的表和它们的关系如下图所示。 其中几个类的具体情况是 可以说这真的是最精简版的博客数据库了。当然数据库中实际存在的还是有 Django 自带的权限表 Group 和 User。为了使用 Vditor,实际实现时还是要把 TextField 替换成 VditorTextField,才能在 Django Admin 中使用 Vditor 进行编辑。 视图和模板视图这部分没什么好说的,主要是以下几个视图: Post 的增删查改 User 的登入登出 Home 登入登出功能主要是限制 Post 增删改的权限。但是如果不要求在前端进行文章操作,完全可以不要登入登出和 Post 的增删改操作(比较适合导出成静态页面)。 主页 Home 的渲染是根据 Profile 中关联的第一个 User 表中用户的记录。由于这个项目在使用 Docker 部署的时候,必然会创建一个超级用户,这个超级用户就是作为第一个用户存在的。Home 中最左侧的介绍以及中间的头像,就是从数据库中取出这个超级用户的相应记录进行渲染的。 模板几乎与视图一一对应,只不过为了避免写重复的代码,将模板进行了分解,使用 Django 提供的 extends 关键词进行模板扩展。主要继承关系如下所示。 日后还可以在此基础上添加其他的模块,例如相册等。 部署该项目提供了 Dockerfile 用于构建 Docker 镜像,可以直接使用 Docker 部署。事实上,该项目就是利用 VSCode 的远程开发功能在 Docker 容器里面开发的。同时仓库中也提供了一个 docker-compose 配置样例,可以直接部署。下图是在服务器上部署后的效果。 相应的 docker-compose 配置如下: 123456789101112131415161718192021222324252627282930313233version: '3.8'services: app: image: hpdell/myzone-django:latest volumes: - upload-data:/code/uploads ports: - \"8080:8000\" depends_on: - db links: - \"db:db\" restart: unless-stopped environment: - DJANGO_SUPERUSER_USERNAME= - DJANGO_SUPERUSER_EMAIL= - DJANGO_SUPERUSER_PASSWORD= - MYZONE_HOST=huyg.site db: image: postgres:latest restart: unless-stopped volumes: - postgres-data:/var/lib/postgresql/data environment: POSTGRES_USER: POSTGRES_DB: POSTGRES_PASSWORD:volumes: postgres-data: null upload-data: null 注意配置中将 uploads 文件夹(上传的图片所保存的位置)永久性保留了下来,这样每次代码更新,直接拉取最新镜像并重新部署容器即可(在 Protainer 中甚至只需要点点鼠标)。 结语整个博客开发时间,满打满算可能有四天,就已经实现了绝大多数功能(有一些在文章中没有介绍,比如本地化、移动端适配),相比之前那个动态博客,开发难度降低了很多,而且可扩展性也很强。这得益于 Django 的强大功能,以及最简化的项目框架,这样我觉得没有选择最火热的 Vue/React 和前后端分离框架是一个非常正确的决定。这样我想起一段往事。 去年我在进行毕设答辩的时候,有一个评委老师问我这样一个问题:“你觉得模型是越复杂越好,还是越简单越好?”我给出的回答是:“模型的复杂度要与实际问题相匹配。” 虽然我不知道这个回答有没有让老师满意,但在开发做项目时,我觉得也是同样的情况,用的技术栈也不是越强大越好,而是要和项目内容相匹配。用一些简化的技术去开发复杂的项目,难度一定会越来越大;而用一些太过强大的技术开发简单的项目,反而会带来很多不必要的麻烦。但是,这并不是说 Django 模板系统不强大,只是基于模板的渲染系统在项目规模更大时会变得比较麻烦,此时采用前后端分离的框架会更好。 当然,这个博客还有一些功能有待完善: 草稿功能。思路是在 Post 中增加一个 Draft 字段来表示是不是草稿,前端再增加一个草稿箱的列表页面。 自动保存。思路是借助 Vditor 自带的缓存功能实现。 退出提醒。退出编辑页面时提示用户数据可能没有保存,避免文稿丢失。 希望这个博客能够稳定运行更长的时间。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"Django","slug":"Django","permalink":"http://hpdell.github.io/tags/Django/"},{"name":"网页开发","slug":"网页开发","permalink":"http://hpdell.github.io/tags/网页开发/"},{"name":"Bootstrap","slug":"Bootstrap","permalink":"http://hpdell.github.io/tags/Bootstrap/"}]},{"title":"Windows 创建符号链接的命令 mklink","slug":"windows-cmd-mklink","date":"2022-01-19T13:03:12.000Z","updated":"2022-04-14T16:50:55.581Z","comments":true,"path":"编程/windows-cmd-mklink/","link":"","permalink":"http://hpdell.github.io/编程/windows-cmd-mklink/","excerpt":"","text":"在 Linux 中,命令 ln 可以方便地创建符号链接,而符号链接在系统运行过程中也起到了很重要的作用。 符号链接,可以理解为也是一种文件或目录,有自己的名称,只不过访问这个文件或目录,等同于访问其目标。 在 Windows 中,可以采用 mklink 命令创建符号链接,也能实现和在 Linux 中类似的效果。 命令语法根据官方文档, 该命令的语法如下(注意这是一个 Windows Command Prompt 命令,不能在 Powershell 中使用): 1mklink [[/d] | [/h] | [/j]] <link> <target> 该命令中的两个参数 <link> 和 <target>, <link> 是符号链接的名称,而 <target> 是链接目标的路径。 根据选项 [/d] [/h] [/j] 的不同该命令一共能创建四种符号链接:文件软链接、文件硬链接、目录软链接、目录联接。 参考始终的博客可知: 文件符号链接 文件硬链接 目录符号链接 目录联接 选项 无 /h /d /j 参数 文件 文件 目录 目录 修改同步 是 是 是 是 删除同步 否 否 否 否 资源管理器类型 .symlink 无特殊显示 文件夹 文件夹 资源管理器图标 快捷方式 无特殊显示 文件夹快捷方式 文件夹快捷方式 彻底删除源 删除源路径 删除所有硬链接 删除源路径 删除源路径 注意该命令运行需要管理员权限。 目录符号连接和目录联接对于文件,符号连接和硬链接的区别比较明显,而且和 Linux 中的差不多。但是对于目录,有符号链接和联接两种模式,这两种模式看起来是一样的,有什么区别? 根据 Understanding NTFS Hard Links, Junctions and Symbolic Links](https://www.2brightsparks.com/resources/articles/NTFS-Hard-Links-Junctions-and-Symbolic-Links.pdf)) 中的介绍,这两种模式最大的区别在于使用的技术不同: 目录联接在 Windows 2000 中被引入,主要基于 NTFS 文件系统的 reparse points 特性实现。重定向的目标必须通过绝对路径定义,所有用于确定目标的信息都在这个路径里。而由于是基于 NTFS 文件系统实现的,所以目录联接只能用于本地路径。 目录符号链接在 Windows Vista 中被引入,是一种更高级的快捷方式。重定向的目标可以是本地路径,也可以是通过 SMB 协议挂在的网络路径。 因此,当需要挂在网络路径时,就需要使用 /d 参数创建目录符号链接。如果只是本地路径,那么使用 /j 或者 /d 都可以。 但是,当链接到 OneDrive 中的文件夹时,如果使用符号链接,会造成一些问题。如果文件没有被下载到本地,那么在双击运行时,资源管理器在下载文件后会产生文件冲突的错误。此时就只能使用 /j 选项,创建目录联接,才可以正常使用。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"http://hpdell.github.io/tags/Windows/"}]},{"title":"《风起洛阳》观后感","slug":"wind-from-luoyang","date":"2021-12-31T23:30:44.000Z","updated":"2022-04-14T16:50:55.581Z","comments":true,"path":"随笔/wind-from-luoyang/","link":"","permalink":"http://hpdell.github.io/随笔/wind-from-luoyang/","excerpt":"","text":"在2022年到来之前终于把这个风起洛阳看完了。 总体而言,这个剧比较适合7左右的评分,就是总体上看着也不那么无聊,但是要说有多神,也并没有多神。可能最神的就是黄轩的演技。 纵观全剧,最大的一个问题在于剧中出场的人物太多,发生的事件太多,导致很多角色不免沦为工具,需要推动剧情的时候就出来一下,为剧情服务的痕迹太重。而有时候往往为了使一个人物的一个情节合理,就必须要先出现另一个人物,另一个情节,最终造成了一些逻辑上的不合理。比如柳公,似乎柳公在全剧就只有一个作用,就是让百里弘毅问他天通道人的手札;柳沣,似乎就是让他因为佩娘被杀而起意刺杀武攸决。这些大多数的不合理都被剧情跳进式的节奏掩盖了,但是这恰恰是本剧看了让人觉得一般的原因。最可惜的是七娘,整个角色在剧中一没有什么作用,二没什么成长,白白浪费了宋轶这个演员。 其实就像高秉烛,高秉烛一开始是为了给兄弟们报仇而活,当仇报了,他又需要什么样的目的去活?我觉得角色也是这样,编剧需要给登场的角色一个在剧中存在的合理的理由,他们的存在要有自己目的,而不是仅仅是推动剧情。 另外本剧似乎不够重视告诉观众剧中人物各种行为的动机,因而给人一种观感,就是不知道主角三个人为什么要查案,为什么要拼了命查这个案。导演用了一些碎片化剪辑的手段,将主角之前的事逐渐交代,但是这会让人在了解全貌之前看得云里雾里。在不知道高秉烛的兄弟是怎么死之前,观众虽然知道高秉烛不是一个一般人,但是他为什么要干这些事?在柳襄死了之后,春秋道还没有完全展开之前,百里弘毅又是为了什么要继续查案?武思月身为内卫,在爱上高秉烛之前,为什么要拼死拼活去查一些好像也没必要她亲自去查的案件(当然你说武思月尽忠职守,倒也讲得通)。这个问题要说大也不大,反正无脑也能过去。 本剧还有一个问题,导演为了多线推进,大量使用了一种最简单的,这边剪一点,那边剪一点,以告诉观众两边人在同时干什么。这种剪辑用多了,就给人一种导演图省事的感觉,不去区分在每个事件中各个故事线的主次。唯一精彩的地方是武思月发现高秉烛进了联昉那段。当时是以武思月为主线,高秉烛已经有一两集没怎么出现了,这时观众会和武思月产生共情:高秉烛去哪了?然后突然高秉烛出现,观众和武思月同时恍然大悟。这就是一个精心设计线索的例子。但是其他很多时候,都是不分主次地多线并进。 之前还和女朋友吐槽过,这个剧的感情线比较灾难。首先是王一博和宋轶、黄轩和宋茜,实在是缺乏CP感。其次,百里弘毅和七娘的感情线,在前期很长一段时间没有什么成长,基本就是重复着百里弘毅外冷内热(甚至也看不到内热),七娘献尽殷勤,最可惜的是百里弘毅约工部监修一场戏,七娘突然闯入,百里弘毅想让她离开而说了几句重话,事后马车上说也有真话,本以为此处七娘会有一些心凉,但是事后发现并没有。这就仿佛七娘成为了衬托百里弘毅外冷内热的工具人。这二人直到最后才有所成长,来得实在是太慢了。至于高秉烛和武思月,本来找到天通道人后被困山洞,武思月负伤一场戏是二人感情线的一个重要事件,但是由于发生得太早,观众直到武思月不会死,本来的与死神赛跑失去了紧张刺激感。而且对角色形象的塑造并没有什么作用。就单纯的感情线塑造而言,还真不如最后一集武思月中箭,血从口中流出,回忆杀开始。 其他缺点还有很多。至于本剧好的地方,主要就是黄轩,黄轩对于角色在各个场景的把握实在是太到位了,他和任何角色对戏都尽显主角风范,时时刻刻都有一个主角的气场。然后是宋轶,对百里弘毅爱的表现淋漓尽致。只可惜宋轶好像不是很适合演女将,不然武思月让她演估计会更为出彩一些。还有本剧的场景精致,武戏精彩,可以看到至少前期是把钱花在制作上了。最后本剧节奏在大多数时间还是比较在线的,而且悬疑感营造的还不错,看起来至少不会无聊。当然优点也不只这些,我只是暂时能想起来这些。 还好,在2022年到来之前写完了这篇影评。","categories":[{"name":"随笔","slug":"随笔","permalink":"http://hpdell.github.io/categories/随笔/"}],"tags":[{"name":"影评","slug":"影评","permalink":"http://hpdell.github.io/tags/影评/"}]},{"title":"《一生一世》观后感","slug":"only-and-forever","date":"2021-11-04T23:12:27.000Z","updated":"2022-04-14T16:50:55.561Z","comments":true,"path":"随笔/only-and-forever/","link":"","permalink":"http://hpdell.github.io/随笔/only-and-forever/","excerpt":"","text":"起初看这个剧,是因为女朋友看完了,跟我说这个偶像剧不一样,我就也想看一下,这样可以和她一起讨论。师姐强推《周生如故》,我没看《周生如故》直接看的《一生一世》。 先说说演员。让我感觉最意外的是,居然在演员表里面看到了龙斌和罗海琼。以前《第十放映室》还是龙叔配音的时候,就很喜欢龙叔的声音,没想到这次还能看到他演习。罗海琼是在《大宋提刑官》里面认识的,当时就很喜欢竹瑛姑。现在有快20年过去了,明显感觉她也老了。任嘉伦之前看过一集好像是他和阚清子还是谁演的侦探剧,其他就没看过了。白鹿本人我是第一次看她的电视剧,但是我总觉得她有一个角度特别像李沁。其他的,我感觉白鹿演配音演员的把式还不够像,任嘉伦说话叹气太多,这些都是不足之处。 整个剧的剧情看下来,导演的节奏安排还是有一些问题的。前20集几乎没有什么矛盾冲突,主要的矛盾在于周生辰、佟佳人、王曼、周文川、王英东等感情纠纷,以及时宜和秦婉的矛盾。周生辰和时宜本身根本谈不上矛盾纠纷,前半年通邮件就没有拍,后面认识、求婚以超乎常人的速度发展,虽然亲密举动比较少,但是糖不少,这反而是这个剧比较特别的地方。到了20集之后,矛盾冲突集中爆发,以至于让人觉得时宜每次去周生辰家就没什么好事发生。时宜落水、周文幸之死、周文川黑化、时宜昏迷。但总体看下来,除了周生辰和时宜感情线比较丰满,其他线略显单薄。周家内部的矛盾冲突既没有给人惊心动魄的感觉,也没有给人暗流涌动的感觉,导致比较乏味。 究其原因,我认为只有周文川一个反面角色,反派没有形成集团。即使周文幸、秦婉做出了一些伤害时宜的事,但最后都有解释,是出于好心。如果反派不够强大,则无法体现正派的强大,正反双方的矛盾冲突就难以建立。周文川最后的挣扎,不过也就是用水果刀绑架时宜,没有什么实现谋划,属于激情犯案。因此总体上,这不是智谋的争斗。因此导致这条线不够出彩。说实话,虽然周家的人物关系不一定能比得上荣国府宁国府,但是也不至于只掀起这些水花。 相比之下,感情线显得比较充分,量大管饱。其实在“前世”的设定下,机场偶遇、西安见面、镇江求婚,这个速度其实也可以理解了。虽然现实中这样的事确实不多,不过也有,刘涛就是个例子。虽然说她们这样谈恋爱的确不是一般人能实现的,一般人既没有任嘉伦白鹿的颜值,也没有周生辰、时宜的才能,更没有周家的家底。但是感情本身就是独一无二的,我也只需要看他们是怎么谈恋爱的就好了。 女朋友最感动的地方是时宜昏迷后,周生辰不受时宜父母待见,但是他坚持每天晚上坐在外面陪着,即使脸上有伤,胳膊骨折,直到最后终于被时宜父母接受,后面带着时宜去西安,每天跟她讲自己干了什么。看了之后我也非常感动。前面那么多集的铺垫,就让周生辰这个行为显得很合理,我们能相信他是会这样做的。 我自己还有一个很感动的地方,也许是因为我看偶像剧比较少吧。周生辰和时宜结婚后,周生辰去不来梅,两个人跨时区打电话。这正是我和我女朋友每天都在做的事。当初和女朋友认识,到在一起,就非常有缘分,在这个剧里也看到了一些我们之间的影子。 最后周生辰时宜结婚了,似乎这是他们从来没有怀疑过的事。我,我们,也是。 P.S. 我感觉小仁和圆圆很配。","categories":[{"name":"随笔","slug":"随笔","permalink":"http://hpdell.github.io/categories/随笔/"}],"tags":[{"name":"影评","slug":"影评","permalink":"http://hpdell.github.io/tags/影评/"}]},{"title":"游戏《原神》开服一周年有感","slug":"genshin-impact-1year","date":"2021-09-17T14:42:00.000Z","updated":"2022-04-14T16:50:55.449Z","comments":true,"path":"随笔/genshin-impact-1year/","link":"","permalink":"http://hpdell.github.io/随笔/genshin-impact-1year/","excerpt":"","text":"写于公测一周年之际。 等飞机闲来无事,回顾一下这一年玩原神的历程,以及一些热点问题的思考。 回忆手机端公测第一天,我就下载了原神,B服。但由于那天要参加雅思考试,没有深入体验。后来师弟提到他也在玩原神,但在官服,我就重新下了个官服,注册了新的账号。好在之前没有怎么玩。深入体验之后,游戏中的音乐、美术、台词等等都无比吸引我,这些已经被夸了很多了。 前期的时候,大世界阵容主要是皇女+重云超导普攻流,由于我当时还没玩懂,皇女没敢升级,圣遗物也没有刷,所以极为刮痧,伤害在150左右。后来又了班尼特,输出能力才大幅提升。随后就是甘雨池抽到了卢姥爷,简单强化了一下圣遗物,就已经可以去深境螺旋走一走了。凭借打深渊前6层的原石,我抽到了胡桃。 胡桃应该是我第一个明显感觉到“想要抽到”的角色,最直接的原因,还是那个“嗷”。在XP这个人问题上,倒不是很喜欢米哈游喜欢的元素(懂得都懂),尤其是高跟鞋,感觉用这个角色打架的时候很心疼。正巧,胡桃穿的不是高跟鞋,腿上也没有夸张的黑丝,而是一双带红色装饰的白色长袜,上身也是低调淡雅的黑色上衣,暴露皮肤的部分很少,后面双马尾尽显少女的可爱与灵动。而那把护摩枪,实在和胡桃本身太配了。于是当我看到同玩的同学氪金抽了护摩之后,我也实在忍不住氪了点钱,出了一把护摩和一把狼末,正好胡桃卢姥爷都有武器了。当然除了外观,玩胡桃传说任务的时候,她所展现出的独特的气质与思维,令我不禁赞叹,感觉是个聪明有趣的女孩子。当然胡桃强度很强,但当我刚抽到的时候只觉得容易暴毙,而且由于没有好的圣遗物,输出能力也一般,可以说胡桃是完全依靠形象吸引到我的。 接下来就是温迪,由于同学夸了很久温迪,我也没多想就抽了,正好卡池开了之后第三抽我就抽到了,没有太费功夫。虽然温迪这个小男孩的麻花辫和白袜子很有特点,但我只用来当个聚怪工具人而已。 然后是钟离,也是必抽七神之一。抽钟离之前,意外的出了公子,导致我不得不又氪了点钱抽钟离。生命套很好弄,史莱姆枪也多的是,很好培养。这两个神有一个很大的特点,就是几乎可以无缝嵌入到任何队伍中,而且他们不靠自身练度就可以提高队伍整体能力。 下一个想抽到的就是优菈,完全是因为她输出很高,而且不是火系输出!这点很重要,只有火c很多时候相当不舒服。 之后便是万叶,第一次抽的时候歪了七七。奇妙的是,我有一天看了马老师的动态,她说她打了深渊之后去抽万叶就抽到了,于是我也这样试了一下,居然在第20抽就抽到了,打破了温迪的记录(当时总第34抽抽到)。果然,不愧是叶天帝,用班尼特+万叶+钟离+迪卢克,迪卢克直接翻身,刀刀过万。甚至由于对群能力出众,有时候甚至比胡桃还快。果然没有抽错。 最后一个节目便是等神里。版本更新后第一时间抽,抽到琴团长。于是疯狂肝地图、开宝箱、做任务,最后又氪了30块钱,终于是抽到了神里。天目影打刀很配。神里最戳我xp的一点是从霰步状态出来时,马尾辫一甩的时候,有青春时期的少女感,很清纯的感觉。在叶天帝和讨龙的辅助下,神里现在也能刀刀近万,输出还是很不错的。刷冰套也很顺利,直接出了一个带爆伤的暴击头,完美符合要求。 9月更新之后,因为首充翻倍重置了,所以氪了点钱去抽雷神,但是歪了刻晴。听说胡桃要复刻,现在就不打算抽出雷神了,及时收手。 总体而言,我在玩原神的时候非常快乐,想自己探索就自己探索,想和朋友联机就和朋友联机,灵活性非常高。我也制作了一些原神相关视频。从游戏制作质量上讲,我对这款游戏是非常满意的。 氪金抽卡原神是一款氪金抽卡游戏,也许很多喜欢买断制游戏的玩家、甚至是抵制氪金抽卡游戏的玩家并不喜欢。而当这样一款游戏在全球27国登顶的时候,就产生了一个奇怪的现象——“环大陆好评圈”。当然,并不是说在港澳台以及国外,所有人都喜欢原神,在大陆就所有人都讨厌原神。但数据证明,确实有这种情况存在。国内只有Taptap平台对于原神(手游)的评价比较高,其他基本都在5分左右,刚公测的时候比现在还要糟糕。 但是,我们要知道,玩游戏玩的是游戏内容,而不是商业模式。氪金抽卡还是买断,都是游戏的商业模式而已,尤其对于国产游戏而言,过于强调其商业模式的类型,无异于揠苗助长。因为,一款制作很差的国产游戏并不能仅仅因为他是买断制,就奉若珍宝;如果你认同这个道理,是不是同样地,一款制作优良的国产游戏也不能仅仅因为他是氪金抽卡制,就万人唾骂。但如果我们真的这样做的话,同样也会消耗国人对于国产游戏的情怀,也会遏制国产游戏发展的潜力:如果广大国产游戏玩家,对于买断制是“出必买”,是不是也会被当成换皮游戏的韭菜割呢? 另一方面,很多人都说过,原神可以0氪。确实,原神完全可以0氪,国家队的强度已经很不错了。但我为什么要氪金?我想很多原神玩家氪金,是真心想得到这个角色,我为了我想要的东西花钱,有什么问题吗?换个角度,厂家出了一个我喜欢的产品,我看到后花钱买了,厂家有什么问题吗?真正存在问题的,是“逼氪”,是那种不靠氪金你已经完全无法继续的游戏(硬逼氪),或者不氪金造成的不良体验已经抵消了游戏内容的良好体验(软逼氪),这种良好体验包括情怀、音美、战斗等。 无论是硬逼氪还是软逼氪,为什么我认为其对游戏产业是不好的,因为这属于对用户的一种绑架。就好像如果你经常去一个理发店,工作人员给你推销他们家的产品,你不想买,于是你不买他们就总是给你最后一个剪,或者总是给你剪得不好看,或者总是对你态度很差。这时你怎么办?如果理发店多,你可以换一家理发店。但如果没有其他理发店呢,当然也可以不理发留迪卢克的发型(雾),也可以自己理发,或者就干脆买他家产品,反正用了不会有害,甚至自己还能开心。 但原神是这种情况吗?不是。首先原神不氪金不会被区别对待,也可能活动打不过但最重要的原石总是最简单的难度就能拿到的,其他的无非是一些材料,很容易就能获得,这就相当于你不买理发店的产品,他们只是在你理完发之后背后的汗毛不主动剃掉。而且新角色有试用,还有传说人物关卡可以试用,你可以体验之后再决定买不买。所以原神是氪金抽卡,但不逼氪。 商业模式是否能促进产品成功?我认为可以。如果小米不走互联网手机的路,也许他们卖不了那么多,价格也不会压到那么低。但商业模式是否能决定产品成功与否?我想也不总是。也许有靠营销成功的产品,但产品能否成功,最终还是由产品的品质决定的。如果想通过营销手段来掩盖产品质量的问题,终究是不会长远。 抄袭风波原神公测之初,最大的黑点恐怕是“抄袭”。但如今看来,抄袭的指控恐怕已经不再成立了。且不说拥有东半球最强法务部的任天堂没有状告米哈游,而且其他一众“被抄袭”的公司也没有告米哈游,甚至原神还得了一种大奖;单说原神中所体现的中国文化和中式价值观,是哪个外国的游戏能做出来的吗?恐怕没有。这是神不似。 至于形似不似,就是具体看游戏制作上。美术上,风格相似就是抄袭吗?至少要在建模、贴图等找到极为相似的地方吧。音乐上,恐怕没人会说陈致逸老师的音乐是抄袭。动作上,如果有相似动作就是抄袭,恐怕是不是只有一款游戏可以有走路这个动作? 总的来说,抄袭指的不是创意,而已创意的实现。同样是做网络唤醒(Wake on LAN),GitHub上一搜一大堆,原理甚至都可能是相通的,但是那些代码都是抄袭的吗?新显卡的评测视频都是那么几个测试步骤,那评测视频都是互相抄袭的吗? 其实想想当时《黑神话:悟空》第一支演示视频出来的时候,就有抄袭《只狼:影逝二度》的声音;当《原神》出来的时候,就有抄袭《塞尔达传说:旷野之息》的声音。为什么都是日本游戏被“抄袭”? 文化输出其实文化输出并不是文化类产品的义务,但文化产品天然具有文化输出的能力和效果,不论是有意的还是无意的。原神是否有做到文化输出?当然有。输出的是不是中国文化?是。 其实可以思考一个问题,迪士尼拍的《花木兰》,是中国的题材,体现的是中国文化吗?其实还是美式文化。那原神的蒙徳用欧洲题材,输出的就是欧洲文化吗?用的日本题材就输出的是日本文化吗? 文化是一个比较抽象的概念,不是一个具体的表象。原神出现了很多和风元素,这些不能说是日本文化。因为文化是融合在认识自然的世界观、为人处世的方法论中的,是一举一动的行为准则、一言一行的思维模式中体现的。中国几千年来人们的服饰、语言、礼制等变化了很多,但中国文化反而一直传承并逐渐沉淀,到现如今的社会主义核心价值观。这种文化是几个中国符号、几个华裔演员就能由美国人输出的吗? 相反,日本文化是米哈游能输出的吗? 黎明时刻我认为原神是国产游戏的黎明时刻。 从游戏工业的角度,原神的出现和成功为游戏产业积累了经验、培养了人才。从玩家的角度,原神带给玩家良好的、完整的游戏体验,也没有如逼氪等降低游戏体验的部分。从国人的角度,原神做到了文化输出,让世界开始深入了解中国文化。 但另一方面,原神绝不代表国产游戏所能达到的颠峰,正如黎明后才是真正的日出。原神有没有问题,有很多问题,爬山穿模、数值平衡、体力限制、刷本无聊等等。正因为如此,只有原神是远远不够的。国产游戏需要多种创意、多种模式、多种思想、多种体验的全面开花,百花齐放百家争鸣。我们只能说,在国产游戏发展历史上,原神注定成为浓墨重彩的一笔。 最后,原神策划还是多学习多研究吧,进步空间很大。","categories":[{"name":"随笔","slug":"随笔","permalink":"http://hpdell.github.io/categories/随笔/"}],"tags":[{"name":"原神","slug":"原神","permalink":"http://hpdell.github.io/tags/原神/"}]},{"title":"使用 GitLab Runner 为 R 包配置持续集成服务","slug":"RPackage-GitlabRunner","date":"2020-11-04T20:43:15.000Z","updated":"2022-04-14T16:50:55.409Z","comments":true,"path":"编程/RPackage-GitlabRunner/","link":"","permalink":"http://hpdell.github.io/编程/RPackage-GitlabRunner/","excerpt":"","text":"主要目的众所周知,R 是一个跨平台统计软件,支持 Debian Ubuntu Fedora openSUSE Windows macOS 等多种平台。 R 中提供了众多的软件包,以实现各种功能。这些包被托管在 CRAN 上。 我们如果写了自己的软件包,也是要发布到 CRAN 上,但是其要求会特别严格,需要这些包在所有平台上编译通过。 R 提供了一个命令 R CMD check --as-cran 以在本地实现模拟 CRAN 编译环境的测试命令。 如果我们可以配置一个持续集成服务,在我们代码更新后直接在这些平台上进行测试,如果通过了就发布到 CRAN 上, 不仅可以大大减少手动测试的工作量,也能尽早发现错误。 平台选择但问题是,基于什么平台配置这个持续集成服务? 目前提供持续集成服务的平台非常多,例如 Travis, Jekins, Github, Gitlab …… 这些平台有的可以自己部署,有的只能使用公有服务;有的使用脚本配置,有的基于 Docker 。 面对这么多平台,我们需要考虑到 R 和 R 包编译环境本身的特点: 与其他软件相比,R 并不那么常用,所以几乎没有现成的 Docker 镜像可以使用; 检查 R 包所需要的依赖包可能非常多,光是 R CMD check 命令执行所需要的包就有十数个,编译环境的配置比较复杂; R 包所需要测试通过的平台比较多,尤其涉及 Windows ,这些平台环境的配置方法也不甚相同。 最后基本没得选,只剩下了 GitLab Runner ,几乎只有它能同时满足以上条件。 而且我们之前也部署了一个 GitLab ,使用 GitLab Runner 顺理成章。 但是由于需要测试这么多平台,虽然我们有一个 ESXi 平台,但是创建虚拟机有点太奢侈和复杂了,毕竟 GitLab Runner 也就是个小软件。 我们可以使用 Docker 容器替代虚拟机。使用 Docker 容器,我们只需要下载相应的镜像,创建容器,在容器中配置环境即可。 由于缺乏经验,能力尚不足以直接构建好相应的 R 镜像,因此我们直接下载相应系统的官方镜像,然后在容器中直接配置环境。 综上,我们选定如下部署路线: 下载相应系统的镜像,创建容器 在容器中安装 R 软件 在容器配置 R 包编译环境 在容器中安装 GitLab Runner 并进行注册 在 GitLab 仓库中编写持续集成配置文件 根据以上路线,我们成功配置好了 5 个 GitLab Runner ,实现了在不同系统上的持续集成。 部署方法创建容器下载镜像和创建容器就详解了,网上到处都是资料。 而且由于我们是在群晖中部署的,所以基本只是点了点鼠标。 命令行的方法我就暂时不介绍了。 所需要注意的是对于 Fedora 系统,为了方便配置 GitLab Runner ,启动脚本请使用 /sbin/init 而不要使用 /bin/bash 以防系统无法启动服务,而且还要使用高级权限运行。 安装 R 软件首先需要安装 R 。在不同系统上安装 R 的方法,都写在 CRAN 中。 这里总结一下 Linux 系统的安装方法,因为 Windows 和 macOS 有图形界面,安装很方便。 Ubuntu修改 apt 源,将以下内容放到 /etc/apt/sources.list (或 /etc/apt/sources.list.d 目录下的文件)中 1deb http://<CRAN地址>/bin/linux/ubuntu <系统版本代号>-<R版本号>/ CRAN 地址可以使用官方地址,但是国内速度较慢,不建议使用。可以使用如下镜像地址: 清华大学: mirrors.tuna.tsinghua.edu.cn/CRAN GWmodel : gwmodel.whu.edu.cn/mirrors/CRAN 使用方法就是把后面的域名和路径替换 <CRAN地址> 的部分。 系统版本代号可以使用命令 cat /etc/os-release 查看,几个 LTS 版本的版号如下 版本号 系统版本代号 20.04 focal 18.04 bionic 16.04 xenial R 版本号取决于要安装哪个 R 的版本 版本号 填写内容 4.0 cran40 3.5, 3.6 cran35 例如,如果想使用 GWmodel 的 CRAN 镜像地址,那么就使用如下 apt 源 1deb http://gwmodel.whu.edu.cn/mirrors/CRAN/bin/linux/ubuntu bionic-cran40/ 然后,认证密钥,使用如下命令 1apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9 之后使用 apt update 和 apt install 即可安装。 Debian安装方法总体和 Ubuntu 一样,但是系统版本代号不太一样,但是一样可以通过 cat /etc/os-release 查看 版本号 系统版本代号 10 buster 9 stretch 8 jessie 然后使用如下命令认证密钥 1apt-key adv --keyserver keys.gnupg.net --recv-key 'E19F5F87128899B192B1A2C2AD5F960A256A04AF' 之后同样使用 apt 安装即可。 openSUSE这个安装比较简单,但是我在使用 Leap 15.2 版本的时候总是会卡住,所以最好还是使用 Leap 15.1 或版本。安装方法是运行如下命令 123VERSION=$(grep \"^PRETTY_NAME\" /etc/os-release | tr \" \" \"_\" | sed -e 's/PRETTY_NAME=//' | sed -e 's/\"//g')zypper addrepo -f http://download.opensuse.org/repositories/devel\\:/languages\\:/R\\:/patched/$VERSION/ R-basezypper install R-base=4.0.3 指定版本号非常重要,不然会安装 R 3.5 版本。 Fedora这个安装更加简单,官方库自带了 R ,所以直接使用如下命令安装 1dnf install R 需要注意的是, Fedora 32 和 33 有 4.0 版本的 R ,之前的版本只有 3.6 版本的 R 。 配置 R 包编译环境主要就是安装依赖包了,这个就和要进行持续集成的 R 包本身相关了。 可以直接运行一次 R CMD check 命令查看有哪些依赖包没有安装。 安装包推荐设置 CRAN 镜像,这时的设置方法是将如下语句放在 Rprofile 文件中(根据使用的镜像不同选择不同的地址) 1234# 清华大学 CRAN 镜像options(\"repos\" = c(CRAN=\"https://mirrors.tuna.tsinghua.edu.cn/CRAN/\"))# GWmodel CRAN 镜像options(\"repos\" = c(CRAN=\"http://gwmodel.whu.edu.cn/mirrors/CRAN/\")) 如果你的包依赖了 sf 包,那么在各个系统下需要安装如下依赖库(不是在 R 中安装) Ubuntu & Debian : libgdal-dev, libudunits2-dev Fedora: gdal-devel, libproj-devel, geos-devel, udunits2-devel, sqlite-devel openSUSE: gdal-devel, libproj-devel, geos-devel, gdal, proj 然后 udunits2 手动编译安装 其他依赖库也都是通过包管理器安装,这样 R 包的依赖才能通过。 安装 GitLab Runner 并进行注册Windows 和 macOS 系统就不说了,非常简单。 Ubuntu 和 Debian 上也有官方教程,直接可以照着做。 Fedora 和 openSUSE 就会遇到奇奇怪怪的问题,尤其是运行 gitlab-runner install 会报系统不支持。 这里主要总结一下 Linux 系统的。 Ubuntu 和 Debian 上的安装Ubuntu, Debian 可以参考官方文档)进行安装,推荐使用包管理方式。如果有依赖问题解决以来问题即可。 Fedora 32 上的安装之所以说 Fedora 32 ,因为官方没有明确支持这个版本,支持 Fedora 30 ,但是 Fedora 30 并没有 R 4.0 的包。但是经过实测, Fedora 32 确实可以安装 GitLab Runner 只是要解决一些问题。所以这个方法只保证这个系统有效,其他版本并不保证。 安装前,容器中可能没有 service 命令,可以通过安装 initscripts 命令进行解决。 此外建议安装 lsb 包。 还记得一开始说容器的启动脚本要使用 /sbin/init 吗?这使得容易可以启动服务。如果使用 /bin/bash 启动的话,即使装了 service 命令,也可能无法启动服务。 然后使用官方软件包进行安装 1234curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | bashexport GITLAB_RUNNER_DISABLE_SKEL=truednf install gitlab-runnergitlab-runner install 这样应该就安装好了。 openSUSE Leap 15.1 上的安装这个明确的讲,使用的是奇技淫巧,不能保证永远可行。 首先下载 gitlab-runner 二进制文件并放到 PATH 路径中(架构根据实际情况调整) 1sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64 然后安装 initscripts 包,并准备以下文件,可以从 Ubuntu 中复制 /etc/init.d/gitlab-runner /lib/lsb/init-functions 然后建立 /etc/rc.d/rc0.d 文件夹,在其中建立软链接 1ln -s /etc/init.d/gitlab-runner /etc/rc.d/rc0.d/K20gitlab-runner 然后,直接运行 gitlab-runner start 应该就可以启动了。 其实,gitlab-runner install 命令只是创建以下服务,这个过程手动创建也是可以的。 上面就是模拟了手动创建的过程。如果你水平比较高,可以自己写服务启动脚本,就不需要从 Ubuntu 复制了。 GitLab Runner 的注册注册非常简单,在所有系统上都是一样的,使用如下命令 1gitlab-runner register 然后程序会问几个问题 GitLab 网址 Runner 注册的 Token Runner 的名字 Runner 的标签 Runner 的运行方式(这里选择 shell ) 以上问题可以根据官方文档进行填写,网上资料也比较多。 注册完,就可以使用如下命令启动了 1gitlab-runner start 在 GitLab 仓库中编写持续集成配置文件如何编写持续集成配置文件,是个非常复杂的问题。这篇博客就不详细讲解了。 这里贴出来我们写好的文件,然后提几个注意事项 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485stages: - build - testvariables: GWMODEL_VERSION: 2.2-0build: stage: build tags: - GWmodel - Ubuntu only: refs: - master script: - R CMD build GWmodel artifacts: paths: - GWmodel_$GWMODEL_VERSION.tar.gztest_cran_ubuntu: stage: test tags: - GWmodel - Ubuntu only: refs: - master script: - R CMD check GWmodel_$GWMODEL_VERSION.tar.gz --as-cran dependencies: - buildtest_cran_debian: stage: test tags: - GWmodel - Debian only: refs: - master script: - R CMD check GWmodel_$GWMODEL_VERSION.tar.gz --as-cran dependencies: - buildtest_cran_openSUSE: stage: test tags: - GWmodel - openSUSE only: refs: - master script: - R CMD check GWmodel_$GWMODEL_VERSION.tar.gz --as-cran dependencies: - buildtest_cran_Fedora: stage: test tags: - GWmodel - Fedora only: refs: - master script: - R CMD check GWmodel_$GWMODEL_VERSION.tar.gz --as-cran dependencies: - buildtest_cran_windows: stage: test tags: - GWmodel - Windows only: refs: - master script: - R.exe CMD check GWmodel_$GWMODEL_VERSION.tar.gz --as-cran --no-manual dependencies: - build Stages 和任务yml 文件第一级的标签名,默认为是任务名,除非是一些特别的关键字,如 stages 。 这里面标识了不同任务所处的阶段,按顺序排列,名字随意。 只有一个阶段的任务全部通过,才执行下一个阶段的任务。 这里第一个阶段 build 构建一个源码包。这个阶段就无需分系统进行,只有测试才需要分系统进行。 任务中的 tags一个任务执行只使用一个 Runner ,具体使用哪个 Runner ,就是通过 tags 选择的。 所以这里的 tags 要和创建 Runner 时设置的 tags 相对应。 任务中的 artifacts 和 dependencies如果一个任务想留下一些东西给后续任务使用,就需要使用 artifacts 指定留下哪些东西。 这些东西也可以在 GitLab 上进行下载。 这里我留下了 build 命令生成的压缩包,供后续 check 过程使用。 如果一个任务想使用之前任务留下来的东西,就需要使用 dependencies 指定获取哪些任务遗留下来的文件。 因此,所有执行 test 阶段的任务都依赖于 build 那个任务即可。 其实最后还应该有一步部署,理论上可以直接上传到 CRAN 中。但是这个还没研究出来怎么做,先挖个坑。 使用 R CMD check --as-cran 会尝试编译文档,因此需要 latex 。这个最好还是安装以下。 如果不想安装,可以仿照 test_cran_windows 中添加 --no-manual 参数,就不会编译 pdf 版的文档了。 以上就是使用 GitLab Runner 进行 R 包持续集成的所有配置方法。 如有不详细的地方,还请在评论区指出。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"R","slug":"R","permalink":"http://hpdell.github.io/tags/R/"},{"name":"GitLab Runner","slug":"GitLab-Runner","permalink":"http://hpdell.github.io/tags/GitLab-Runner/"}]},{"title":"QGIS 二次开发笔记(3)——空间距离和空间权重","slug":"qgisdev3-spatialweight","date":"2020-08-12T17:50:31.000Z","updated":"2022-04-14T16:50:55.573Z","comments":true,"path":"编程/qgisdev3-spatialweight/","link":"","permalink":"http://hpdell.github.io/编程/qgisdev3-spatialweight/","excerpt":"","text":"这个博客如果只是一个复刻 QGIS 的教程的话,就没有什么价值,大家只要照着 QGIS 中的复制粘贴就可以了。所以这篇博客先来介绍一些 QGIS 中没有的。 我们使用 QGIS 做二次开发的目的,无非就是在软件中集成我们研发的一些算法,尤其是空间算法,不论是针对矢量数据还是栅格数据(我们主要研究矢量数据)。 对于矢量数据的空间算法而言,空间距离和空间权重非常重要,因为其反映了地理学第一定律: 任何事物都是与其他事物相关的,只不过相近的事物关联更紧密。 我们可以看到,不论是在莫兰指数(Moran’s I)中,还是空间自回归模型(SAR)中,或者地理加权回归模型(GWR)中,空间权重都是非常重要的。 空间权重空间权重的计算可以是多种多样的,除了有一条总的原则:权重随距离的增加而减小,即权重是距离的单调减函数。该函数可称为“空间权重核函数”,即 $w(d)$。 该函数还可以引入一些参数,如 $w(d;b)$ ,参数 $b$ 可以事先指定,或者根据优化算法进行优化。 在莫兰指数、空间自回归模型等算法中,常常使用不含参数的空间权重函数,如“平方反距离函数”(忽略 $d=0$ 的情况) $$ w = \\frac{1}{d^2} $$ 或者“指数反距离函数” $$ w = e^{-d} $$ 由于权重只有相对大小有意义,因此这些函数可以不用像概率密度函数一样要求 $\\int w \\ \\mathrm{d}d = 1$ 。 在核密度分析、地理加权回归分析等算法中,常常使用含有一个参数 $b$ 的空间权重函数,该参数被称之为“带宽”(bandwidth)。 例如常用的高斯权重核函数 $$ w(d;b) = exp\\left{ -\\frac{1}{2} \\left(\\frac{d}{b}\\right)^2 \\right} $$ 该函数在任何距离上都有一定的权重,即使权重非常小,被称之为“非截断型”权重核函数。 还有一种权重核函数,如“双平方权重核函数” $$ w(d;b) = \\left{ \\begin{matrix} \\left( 1 - \\left( \\frac{d}{b} \\right)^2 \\right)^2 & d \\leq b \\ 0 & d> b \\end{matrix} \\right. $$ 即超过带宽范围的位置上权重均为 $0$ ,被称之为“截断型”权重核函数。 空间距离空间权重是根据距离计算的,而如何定义距离,也是个非常重要的问题。 我们最常用的就是欧氏距离(投影坐标系)或大圆距离(地理坐标系)。 除此之外,还有其他一些会用到的距离计算方法。 地理加权回归分析中,也经常用到以下几个距离: 曼哈顿距离:$D_{1,2}=|x_1-x_2|+|y_1-y_2|$ 闵可夫斯基距离:$D_{1,2}=\\left(|x_1-x_2|^p+|y_1-y_2|^p\\right)^{\\frac{1}{p}}$ 路网距离:道路网络上两点的最短距离 对于栅格数据,或者栅格采样的点数据,也可以采用“四邻域距离”、“皇后距离”等等。 时空地理加权回归分析中,采用了一个“时空距离”的概念,即将时间和空间组合到一起。 原作者提供的思路是 $$ D_{1,2} = \\mu((x_1-x_2)^2+(y_1-y_2)^2)+\\lambda(t_1-t_2)^2 $$ 其中 $\\mu+\\lambda=1$ 且 $\\mu,\\lambda>0$ ,并选择合适的值以平衡时间和空间因素。 如果数据是线数据,那又该如何定义距离呢?目前有几种定义 Flow 距离的方式,但是总体上不是很令人满意。 而距离又需要满足非负性、同一性、对称性和三角不等式,因此往往需要根据实际研究内容的特点设计距离。 空间权重和距离的程序实现根据以上介绍可以发现,空间权重离不开距离,各自又都多种多样,而且独立于算法。 在面向对象语言中,我们可以使用继承和多态特性,实现对空间权重和距离的封装: 将“空间权重”、“空间距离”分别定义为基类,再从其中派生出各种具体的权重和距离。 根据需要,将权重和距离进行组合,提供统一接口计算权重。 空间权重的声明如下: 123456789101112131415class QgsdkWeight{public: enum WeightType { BandwidthWeight };public: QgsdkWeight() {} virtual ~QgsdkWeight() {} virtual QgsdkWeight* clone() = 0;public: // 求权重向量 virtual vec weight(vec dist) = 0;}; 空间距离的声明如下: 12345678910111213141516171819202122232425262728293031class QgsdkDistance{public: enum DistanceType { CRSDistance, MinkwoskiDistance, DMatDistance };public: explicit QgsdkDistance(int total) : mTotal(total) {}; QgsdkDistance(const QgsdkDistance& d) { mTotal = d.mTotal; }; virtual ~QgsdkDistance() {}; virtual QgsdkDistance* clone() = 0; virtual DistanceType type() = 0; int total() const; void setTotal(int total);public: // 计算距离的函数 virtual vec distance(int focus) = 0; // 返回结果的元素个数 virtual int length() const = 0; // 求最大距离 double maxDistance(); // 求最小距离 double minDistance();protected: int mTotal = 0;}; 这里只是用一个 focus 整型变量来指定计算当前数据中第 $i$ 个点到其他所有点的距离。 在该设计下,需要将所有用于计算的数据保存在 QgsdkDistance 实例中,这是为了避免不同派生类所需参数不同的问题。 但也可以采用另一种方法,即接受一个 void * 类型的参数,这个参数指向计算所需要的所有数据,即可实现不同类型参数的传递。 或者也可以使用 Qt 中特有的 QVariant 类型,以在派生类中实现不同类型参数的传递。 然后可以构建一个组合类,将权重和距离组合起来: 12345678910111213141516171819202122232425262728class QgsdkSpatialWeight{public: QgsdkSpatialWeight(); QgsdkSpatialWeight(QgsdkWeight* weight, QgsdkDistance* distance); QgsdkSpatialWeight(const QgsdkSpatialWeight& spatialWeight); ~QgsdkSpatialWeight(); QgsdkWeight *weight() const; void setWeight(QgsdkWeight *weight); void setWeight(QgsdkWeight& weight); QgsdkDistance *distance() const; void setDistance(QgsdkDistance *distance); void setDistance(QgsdkDistance& distance);public: QgsdkSpatialWeight& operator=(const QgsdkSpatialWeight& spatialWeight); QgsdkSpatialWeight& operator=(const QgsdkSpatialWeight&& spatialWeight);public: virtual vec weightVector(int i); virtual bool isValid();private: QgsdkWeight* mWeight = nullptr; QgsdkDistance* mDistance = nullptr;}; 然后分别对 QgsdkWeight 和 QgsdkDistance 进行具体实现,如带宽权重和欧式/大圆距离(隐去 get/set 函数): 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152class QgsdkBandwidthWeight : public QgsdkWeight{public: typedef double (*KernelFunction)(double, double); static KernelFunction Kernel[]; static double GaussianKernelFunction(double dist, double bw); static double ExponentialKernelFunction(double dist, double bw); static double BisquareKernelFunction(double dist, double bw); static double TricubeKernelFunction(double dist, double bw); static double BoxcarKernelFunction(double dist, double bw);public: QgsdkBandwidthWeight(); QgsdkBandwidthWeight(double size, bool adaptive, KernelFunctionType kernel); QgsdkBandwidthWeight(const QgsdkBandwidthWeight& bandwidthWeight); QgsdkBandwidthWeight(const QgsdkBandwidthWeight* bandwidthWeight); virtual QgsdkWeight * clone() override;public: virtual vec weight(vec dist) override;private: double mBandwidth; bool mAdaptive; KernelFunctionType mKernel;};class QgsdkCRSDistance : public QgsdkDistance{public: static vec SpatialDistance(const rowvec& out_loc, const mat& in_locs); static vec EuclideanDistance(const rowvec& out_loc, const mat& in_locs); static double SpGcdist(double lon1, double lon2, double lat1, double lat2);public: explicit QgsdkCRSDistance(int total, bool isGeographic); QgsdkCRSDistance(const QgsdkCRSDistance& distance); virtual QgsdkDistance * clone() override; DistanceType type() override { return DistanceType::CRSDistance; }public: virtual vec distance(int focus) override; int length() const override;protected: bool mGeographic = false; mat* mFocusPoints = nullptr; mat* mDataPoints = nullptr;}; 按照当前设计,这个 QgsdkCRSDistance 在使用的时候,就需要预先设置 mFocusPoints 和 mFocusPoints 属性。 如果指定 mGeographic 为 true 则按照大圆距离计算,反之按照欧氏距离计算。 这个 QgsdkBandwidthWeight 可以根据指定的不同的核函数进行计算。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"Qt","slug":"Qt","permalink":"http://hpdell.github.io/tags/Qt/"},{"name":"QGIS","slug":"QGIS","permalink":"http://hpdell.github.io/tags/QGIS/"}]},{"title":"QGIS 二次开发笔记(2)——显示图层","slug":"qgisdev2-mapcanvas","date":"2020-07-07T21:00:52.000Z","updated":"2022-04-14T16:50:55.573Z","comments":true,"path":"编程/qgisdev2-mapcanvas/","link":"","permalink":"http://hpdell.github.io/编程/qgisdev2-mapcanvas/","excerpt":"","text":"基于 QGIS 二次开发,最首要的功能就是显示图层。这是个看似非常简单的功能,但是在 QGIS 中写了非常复杂的代码,以支持各种数据源。 但是我们在二次开发中,一般不会支持那么多的数据源。这篇博客首先以 ESRI Shapefile 数据源为例,展示加载图层的过程。 博客以创建好的工程开始,创建工程的过程网上资料很多,这里就不再赘述了。 添加地图框要想显示图层,首先要有一个显示图层的地方。在 QGIS SDK 中,使用类 QgsMapCanvas 显示地图。这个类需要 QT 中的 svg 组件,即 12# QgsSdkApp.proQT += core gui xml svg 如果我们想在 QMainWindow 派生类(我的工程中创建的类名是 QgsSdkApp ,以后直接用这个名称)添加一个 QgsMapCanvas 类型的组件, 有以下三种方法: 添加插件的方式:在 QT Designer 中添加插件,在 QT Designer 中绘制(我没有实现成功,理论上可以) 提升类型的方式:在 QT Designer 中使用“提升”功能,将 QWidget 组件提升为 QgsMapCanvas 类型 手动创建的方式:在 QgsSdkApp.cpp 中手动创建 QgsMapCanvas 类型的对象,添加到窗口中 下面一一展示。 提升类型的方式选择一个组件,右键单击之后,单击“提升为”按钮,可以弹出提升窗口部件的对话框。 上面这个对话框,现在 1 所示的位置输入要提升的类型的名称,然后点击 2 位置上的按钮,在上面的列表上选中刚刚添加的提升类型, 点击 3 位置上的按钮,即可将该组件提升为指定的类型(即 QgsMapCanvas 类型)。 然后,在 cpp 文件中已经可以访问到这个类型的组件了。 手动创建的方式手动创建的方式就是在窗口类的构造函数中,构造一个 QgsMapCanvas 类型的对象,然后添加到窗口上。 这种情况下和其他在 QT 中手动创建对象没有差别,和其他一样处理即可。 添加图层添加了地图框(在类内使用一个指针 mMapCanvas ,指向这个地图框)之后,下面就可以来添加图层了。 首先以 ESRI Shapefile 为例,介绍一下添加 QgsVectorLayer 的基本方法。 添加矢量图层的方法 在 QgsSdkApp.ui 中可以添加一个 action ,并拖到工具栏上创建工具按钮。点击这个按钮后开始添加图层。 这个过程网上有很多教程,这里就不再赘述了。 如果要添加 ESRI Shapefile 图层,我们需要先选择这个文件。我们可以直接弹出一个文件选择对话框。 我们以选择的文件路径作为数据源的路径,文件名(不含扩展名)为图层名称。 12345678910void QgsSdkApp::on_actionShp_Layer_triggered(){ QString filePath = QFileDialog::getOpenFileName(this, tr(\"Open ESRI Shapefile\"), tr(\"\"), tr(\"ESRI Shapefile (*.shp)\")); QFileInfo fileInfo(filePath); if (fileInfo.exists()) { QString fileName = fileInfo.baseName(); addVectorLayer(filePath, fileName, \"ogr\"); }} 创建 addVectorLayer() 函数,用于添加图层。我们可以以一种简单的方式添加图层,即直接创建 QgsVectorLayer 对象,并保存到一个列表(mMapLayerList)里。 然后让地图框加载这个列表,即可显示地图。 12345678910111213QgsVectorLayer *QgsSdkApp::addVectorLayer(const QString &vectorLayerPath, const QString &name, const QString &providerKey, bool guiWarning){ QgsVectorLayer* vectorLayer = new QgsVectorLayer(uri, layerName, providerKey); mMapLayerList.append(vectorLayer); mMapCanvas->setLayers(mMapLayerList); if (mMapLayerList.size() == 1) { QgsMapLayer* firstLayer = mMapLayerList.first(); QgsRectangle extent = mMapCanvas->mapSettings().layerExtentToOutputExtent(firstLayer, firstLayer->extent()); mMapCanvas->setExtent(extent); } mMapCanvas->refresh();} 这个函数中除了添加了地图,同时也限制了地图框地显示范围,即设置为第一个图层地显示范围。最后对地图进行了刷新。但是这种方式会遇到很多问题: 如果将来要设计 Layout ,那么这个地图框的内容无法同步到 Layout 中的地图框中。 如果图层的投影到地图框的投影有多种转换方式,那么无法选择指定投影方式(尚未实现成功) 如果图层有子图层,无法选择子图层(ESRI Shapefile 中不会遇到) 如果图层需要访问验证,无法获取图层(ESRI Shapefile 中不会遇到) 在 QGIS 中,使用以下代码以较为完善地设置添加地图层,同时支持了很多其他功能。函数中创建的图层直接添加到 QgsProject 中,以支持 Layout 等功能。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091QgsVectorLayer *QgsSdkApp::addVectorLayer(const QString &vectorLayerPath, const QString &name, const QString &providerKey, bool guiWarning){ QString baseName = QgsMapLayer::formatLayerName( name ); /* Eliminate the need to instantiate the layer based on provider type. The caller is responsible for cobbling together the needed information to open the layer */ QgsDebugMsg( \"Creating new vector layer using \" + vectorLayerPath + \" with baseName of \" + baseName + \" and providerKey of \" + providerKey ); // if the layer needs authentication, ensure the master password is set bool authok = true; QRegExp rx( \"authcfg=([a-z]|[A-Z]|[0-9]){7}\" ); if ( rx.indexIn( vectorLayerPath ) != -1 ) { authok = false; if ( !QgsAuthGuiUtils::isDisabled( messageBar(), messageTimeout() ) ) { authok = QgsApplication::authManager()->setMasterPassword( true ); } } // create the layer QgsVectorLayer::LayerOptions options { QgsProject::instance()->transformContext() }; // Default style is loaded later in this method options.loadDefaultStyle = false; QgsVectorLayer *layer = new QgsVectorLayer( vectorLayerPath, baseName, providerKey, options ); if ( authok && layer && layer->isValid() ) { QStringList sublayers = layer->dataProvider()->subLayers(); QgsDebugMsg( QStringLiteral( \"got valid layer with %1 sublayers\" ).arg( sublayers.count() ) ); // If the newly created layer has more than 1 layer of data available, we show the // sublayers selection dialog so the user can select the sublayers to actually load. if ( sublayers.count() > 1 && ! vectorLayerPath.contains( QStringLiteral( \"layerid=\" ) ) && ! vectorLayerPath.contains( QStringLiteral( \"layername=\" ) ) ) { QList< QgsMapLayer * > addedLayers = askUserForOGRSublayers( layer ); // The first layer loaded is not useful in that case. The user can select it in // the list if he wants to load it. delete layer; layer = addedLayers.isEmpty() ? nullptr : qobject_cast< QgsVectorLayer * >( addedLayers.at( 0 ) ); for ( QgsMapLayer *l : addedLayers ) askUserForDatumTransform( l->crs(), QgsProject::instance()->crs(), l ); } else { // Register this layer with the layers registry QList<QgsMapLayer *> myList; //set friendly name for datasources with only one layer QStringList sublayers = layer->dataProvider()->subLayers(); if ( !sublayers.isEmpty() ) { setupVectorLayer( vectorLayerPath, sublayers, layer, providerKey, options ); } myList << layer; QgsProject::instance()->addMapLayers( myList ); askUserForDatumTransform( layer->crs(), QgsProject::instance()->crs(), layer ); bool ok; layer->loadDefaultStyle( ok ); layer->loadDefaultMetadata( ok ); } } else { if ( guiWarning ) { QString message = layer->dataProvider() ? layer->dataProvider()->error().message( QgsErrorMessage::Text ) : tr( \"Invalid provider\" ); QString msg = tr( \"The layer %1 is not a valid layer and can not be added to the map. Reason: %2\" ).arg( vectorLayerPath, message ); visibleMessageBar()->pushMessage( tr( \"Layer is not valid\" ), msg, Qgis::Critical, messageTimeout() ); } delete layer; return nullptr; } // Let render() do its own cursor management // QApplication::restoreOverrideCursor(); return layer;} 注意其中的 QgsProject::instance()->addMapLayers( myList ); 一行,其实在 QGIS 中所有图层都是通过 QgsProject 进行管理的,一般来说用户无需自己管理。 QGIS SDK 也提供了 QgsLayerTreeView 等一套类,用于显示图层的层级结构,使用也比较方便。 但是如果有一些特别需要的功能,再进行自己管理。 这个函数中还需要其他几个函数,分别定义如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218QList<QgsMapLayer *> QgsSdkApp::askUserForOGRSublayers(QgsVectorLayer *layer){ QList<QgsMapLayer *> result; if ( !layer ) { layer = qobject_cast<QgsVectorLayer *>( activeLayer() ); if ( !layer || layer->providerType() != QLatin1String( \"ogr\" ) ) return result; } QStringList sublayers = layer->dataProvider()->subLayers(); QgsSublayersDialog::LayerDefinitionList list; QMap< QString, int > mapLayerNameToCount; bool uniqueNames = true; int lastLayerId = -1; const auto constSublayers = sublayers; for ( const QString &sublayer : constSublayers ) { // OGR provider returns items in this format: // <layer_index>:<name>:<feature_count>:<geom_type> QStringList elements = splitSubLayerDef( sublayer ); if ( elements.count() >= 4 ) { QgsSublayersDialog::LayerDefinition def; def.layerId = elements[0].toInt(); def.layerName = elements[1]; def.count = elements[2].toInt(); def.type = elements[3]; // ignore geometry column name at elements[4] if ( elements.count() >= 6 ) def.description = elements[5]; if ( lastLayerId != def.layerId ) { int count = ++mapLayerNameToCount[def.layerName]; if ( count > 1 || def.layerName.isEmpty() ) uniqueNames = false; lastLayerId = def.layerId; } list << def; } else { QgsDebugMsg( \"Unexpected output from OGR provider's subLayers()! \" + sublayer ); } } // Check if the current layer uri contains the // We initialize a selection dialog and display it. QgsSublayersDialog chooseSublayersDialog( QgsSublayersDialog::Ogr, QStringLiteral( \"ogr\" ), this ); chooseSublayersDialog.setShowAddToGroupCheckbox( true ); chooseSublayersDialog.populateLayerTable( list ); if ( !chooseSublayersDialog.exec() ) return result; QString name = layer->name(); auto uriParts = QgsProviderRegistry::instance()->decodeUri( layer->providerType(), layer->dataProvider()->dataSourceUri() ); QString uri( uriParts.value( QStringLiteral( \"path\" ) ).toString() ); // The uri must contain the actual uri of the vectorLayer from which we are // going to load the sublayers. QString fileName = QFileInfo( uri ).baseName(); const auto constSelection = chooseSublayersDialog.selection(); for ( const QgsSublayersDialog::LayerDefinition &def : constSelection ) { QString layerGeometryType = def.type; QString composedURI = uri; if ( uniqueNames ) { composedURI += \"|layername=\" + def.layerName; } else { // Only use layerId if there are ambiguities with names composedURI += \"|layerid=\" + QString::number( def.layerId ); } if ( !layerGeometryType.isEmpty() ) { composedURI += \"|geometrytype=\" + layerGeometryType; } QgsDebugMsg( \"Creating new vector layer using \" + composedURI ); QString name = fileName + \" \" + def.layerName; QgsVectorLayer::LayerOptions options { QgsProject::instance()->transformContext() }; options.loadDefaultStyle = false; QgsVectorLayer *layer = new QgsVectorLayer( composedURI, name, QStringLiteral( \"ogr\" ), options ); if ( layer && layer->isValid() ) { result << layer; } else { QString msg = tr( \"%1 is not a valid or recognized data source\" ).arg( composedURI ); visibleMessageBar()->pushMessage( tr( \"Invalid Data Source\" ), msg, Qgis::Critical, messageTimeout() ); delete layer; } } if ( !result.isEmpty() ) { QgsSettings settings; bool addToGroup = settings.value( QStringLiteral( \"/qgis/openSublayersInGroup\" ), true ).toBool(); bool newLayersVisible = settings.value( QStringLiteral( \"/qgis/new_layers_visible\" ), true ).toBool(); QgsLayerTreeGroup *group = nullptr; if ( addToGroup ) group = QgsProject::instance()->layerTreeRoot()->insertGroup( 0, name ); QgsProject::instance()->addMapLayers( result, ! addToGroup ); for ( QgsMapLayer *l : qgis::as_const( result ) ) { bool ok; l->loadDefaultStyle( ok ); l->loadDefaultMetadata( ok ); if ( addToGroup ) group->addLayer( l ); } // Respect if user don't want the new group of layers visible. if ( addToGroup && ! newLayersVisible ) group->setItemVisibilityCheckedRecursive( newLayersVisible ); } return result;}bool QgsSdkApp::askUserForDatumTransform(const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs, const QgsMapLayer *layer){ Q_ASSERT( qApp->thread() == QThread::currentThread() ); QString title; if ( layer ) { // try to make a user-friendly (short!) identifier for the layer QString layerIdentifier; if ( !layer->name().isEmpty() ) { layerIdentifier = layer->name(); } else { const QVariantMap parts = QgsProviderRegistry::instance()->decodeUri( layer->providerType(), layer->source() ); if ( parts.contains( QStringLiteral( \"path\" ) ) ) { const QFileInfo fi( parts.value( QStringLiteral( \"path\" ) ).toString() ); layerIdentifier = fi.fileName(); } else if ( layer->dataProvider() ) { const QgsDataSourceUri uri( layer->source() ); layerIdentifier = uri.table(); } } if ( !layerIdentifier.isEmpty() ) title = tr( \"Select Transformation for %1\" ).arg( layerIdentifier ); } return QgsDatumTransformDialog::run( sourceCrs, destinationCrs, this, mMapCanvas, title );}static QStringList splitSubLayerDef( const QString &subLayerDef ){ return subLayerDef.split( QgsDataProvider::sublayerSeparator() );}static void setupVectorLayer( const QString &vectorLayerPath, const QStringList &sublayers, QgsVectorLayer *&layer, const QString &providerKey, QgsVectorLayer::LayerOptions options ){ //set friendly name for datasources with only one layer QgsSettings settings; QStringList elements = splitSubLayerDef( sublayers.at( 0 ) ); QString rawLayerName = elements.size() >= 2 ? elements.at( 1 ) : QString(); QString subLayerNameFormatted = rawLayerName; if ( settings.value( QStringLiteral( \"qgis/formatLayerName\" ), false ).toBool() ) { subLayerNameFormatted = QgsMapLayer::formatLayerName( subLayerNameFormatted ); } if ( elements.size() >= 4 && layer->name().compare( rawLayerName, Qt::CaseInsensitive ) != 0 && layer->name().compare( subLayerNameFormatted, Qt::CaseInsensitive ) != 0 ) { layer->setName( QStringLiteral( \"%1 %2\" ).arg( layer->name(), rawLayerName ) ); } // Systematically add a layername= option to OGR datasets in case // the current single layer dataset becomes layer a multi-layer one. // Except for a few select extensions, known to be always single layer dataset. QFileInfo fi( vectorLayerPath ); QString ext = fi.suffix().toLower(); if ( providerKey == QLatin1String( \"ogr\" ) && ext != QLatin1String( \"shp\" ) && ext != QLatin1String( \"mif\" ) && ext != QLatin1String( \"tab\" ) && ext != QLatin1String( \"csv\" ) && ext != QLatin1String( \"geojson\" ) && ! vectorLayerPath.contains( QStringLiteral( \"layerid=\" ) ) && ! vectorLayerPath.contains( QStringLiteral( \"layername=\" ) ) ) { auto uriParts = QgsProviderRegistry::instance()->decodeUri( layer->providerType(), layer->dataProvider()->dataSourceUri() ); QString composedURI( uriParts.value( QStringLiteral( \"path\" ) ).toString() ); composedURI += \"|layername=\" + rawLayerName; auto newLayer = qgis::make_unique<QgsVectorLayer>( composedURI, layer->name(), QStringLiteral( \"ogr\" ), options ); if ( newLayer && newLayer->isValid() ) { delete layer; layer = newLayer.release(); } }} 这个函数中还需要以下几个 Getter 函数,我们在窗口中提供对应的组件即可: QgsMessageBar* messageBar() : 可以在状态栏上创建一个 QgsMessageBar 的对象,用这个函数获取 int messageTimeout() : 可以直接返回 500 ,或根据配置文件、设置等返回 QgsMessageBar* visibleMessageBar() : 可直接返回状态栏上的 QgsMessageBar 对象 QgsMapLayer* activeLayer() : 需要使用 QgsLayerTreeView 类获取,下面详述 但是现在,还不能在地图上显示,需要将 QgsMapCanvas 和 QgsProject 建立关联,才能将 QgsProject 中的图层同步到 QgsMapCanvas 中。 使用的方法是在构造函数中添加如下代码: 1234567891011121314QgsSdkApp::QgsSdkApp(QWidget *parent) : QMainWindow(parent) , ui(new Ui::QgsSdkApp){ ui->setupUi(this); mMapCanvas = ui->centralwidget; mMapCanvas->setObjectName(QStringLiteral(\"theMapCanvas\")); /** [BEGIN] 添加的用于将 `QgsMapCanvas` 和 `QgsProject` 建立关联的代码 */ mLayerTreeCanvasBridge = new QgsLayerTreeMapCanvasBridge(QgsProject::instance()->layerTreeRoot(), mMapCanvas, this); /** [END] */ connect(ui->actionAdd_Shp_Layer, &QAction::triggered, this, &QgsSdkApp::on_actionShp_Layer_triggered);} 这样添加了图层之后,就可以在地图上显示了。 添加其他图层其他图层的添加方法,都可以从 QGIS 的代码中进行参考。在 qgisapp.cpp 文件中,有这个函数 1234567891011121314151617181920212223242526272829303132333435363738394041void QgisApp::dataSourceManager( const QString &pageName ){ if ( ! mDataSourceManagerDialog ) { mDataSourceManagerDialog = new QgsDataSourceManagerDialog( mBrowserModel, this, mapCanvas() ); connect( this, &QgisApp::connectionsChanged, mDataSourceManagerDialog, &QgsDataSourceManagerDialog::refresh ); connect( mDataSourceManagerDialog, &QgsDataSourceManagerDialog::connectionsChanged, this, &QgisApp::connectionsChanged ); connect( mDataSourceManagerDialog, SIGNAL( addRasterLayer( QString const &, QString const &, QString const & ) ), this, SLOT( addRasterLayer( QString const &, QString const &, QString const & ) ) ); connect( mDataSourceManagerDialog, SIGNAL( addVectorLayer( QString const &, QString const &, QString const & ) ), this, SLOT( addVectorLayer( QString const &, QString const &, QString const & ) ) ); connect( mDataSourceManagerDialog, SIGNAL( addVectorLayers( QStringList const &, QString const &, QString const & ) ), this, SLOT( addVectorLayers( QStringList const &, QString const &, QString const & ) ) ); connect( mDataSourceManagerDialog, &QgsDataSourceManagerDialog::addMeshLayer, this, &QgisApp::addMeshLayer ); connect( mDataSourceManagerDialog, &QgsDataSourceManagerDialog::showStatusMessage, this, &QgisApp::showStatusMessage ); connect( mDataSourceManagerDialog, &QgsDataSourceManagerDialog::addDatabaseLayers, this, &QgisApp::addDatabaseLayers ); connect( mDataSourceManagerDialog, &QgsDataSourceManagerDialog::replaceSelectedVectorLayer, this, &QgisApp::replaceSelectedVectorLayer ); connect( mDataSourceManagerDialog, static_cast<void ( QgsDataSourceManagerDialog::* )()>( &QgsDataSourceManagerDialog::addRasterLayer ), this, static_cast<void ( QgisApp::* )()>( &QgisApp::addRasterLayer ) ); connect( mDataSourceManagerDialog, &QgsDataSourceManagerDialog::handleDropUriList, this, &QgisApp::handleDropUriList ); connect( this, &QgisApp::newProject, mDataSourceManagerDialog, &QgsDataSourceManagerDialog::updateProjectHome ); connect( mDataSourceManagerDialog, &QgsDataSourceManagerDialog::openFile, this, &QgisApp::openFile ); } else { mDataSourceManagerDialog->reset(); } // Try to open the dialog on a particular page if ( ! pageName.isEmpty() ) { mDataSourceManagerDialog->openPage( pageName ); } if ( QgsSettings().value( QStringLiteral( \"/qgis/dataSourceManagerNonModal\" ), true ).toBool() ) { mDataSourceManagerDialog->show(); } else { mDataSourceManagerDialog->exec(); }} 这个函数中有 addRasterLayer addVectorLayer addMeshLayer 等函数,是添加不同类型的图层的方法。可以直接查看这些方法,学习其中的方法,放到工程中来。 在下篇博客中,我计划介绍添加 CSV 类型数据的方法。 显示图层树一般情况下我们都需要使用图层树来对图层进行管理。下面我们就在界面上添加 QgsLayerTreeView 对象。 我们首先在界面上创建一个 QWidget 组件,提升为 QgsLayerTreeView 类型。然后再构造函数中给其设置 Model 等。 123456789101112131415161718QgsSdkApp::QgsSdkApp(QWidget *parent) : QMainWindow(parent), ui(new Ui::QgsSdkApp){ ui->setupUi(this); mMapCanvas = ui->centralwidget; mMapCanvas->setObjectName(QStringLiteral(\"theMapCanvas\")); mLayerTreeCanvasBridge = new QgsLayerTreeMapCanvasBridge(QgsProject::instance()->layerTreeRoot(), mMapCanvas, this); /** [BEGIN] 设置 QgsLayerTreeView 的 Model */ QgsLayerTreeModel* model = new QgsLayerTreeModel(QgsProject::instance()->layerTreeRoot(), this); ui->layerTreeView->setModel(model); ui->layerTreeView->setObjectName(QStringLiteral( \"theLayerTreeView\" )); /** [END] */ mInfoBar = new QgsMessageBar(this); ui->statusbar->addWidget(mInfoBar); connect(ui->actionAdd_Shp_Layer, &QAction::triggered, this, &QgsSdkApp::on_actionShp_Layer_triggered);} QT 中采用的 MVC 模型。 QT 中提供了 QTreeView 、 QListView 、 QTableView 等视图(View), 也提供了 QAbstractItemModel 、 QAbstractListModel 、 QAbstractTableModel 三种模型(Model), 也会提供了一些 Delegate 委托(相当于控制器 Controller)。 但是我们这个图层树仅仅有一个最基本的功能,而在 QGIS 中排序、右键菜单丰富的功能。 对于右键菜单,QGIS 中使用了 Provider 的方式提供右键菜单的菜单项,我们需要将这些 Provider 的代码复制过来,添加到工程中。 12// qgisapp.cpp [4493行]mLayerTreeView->setMenuProvider( new QgsAppLayerTreeViewMenuProvider( mLayerTreeView, mMapCanvas ) ); 对于排序等其他功能,我们可以按需添加。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"Qt","slug":"Qt","permalink":"http://hpdell.github.io/tags/Qt/"},{"name":"QGIS","slug":"QGIS","permalink":"http://hpdell.github.io/tags/QGIS/"}]},{"title":"QGIS 二次开发笔记(1)——环境配置","slug":"qgisdev1-build","date":"2020-03-28T10:34:50.000Z","updated":"2022-04-14T16:50:55.561Z","comments":true,"path":"编程/qgisdev1-build/","link":"","permalink":"http://hpdell.github.io/编程/qgisdev1-build/","excerpt":"众所周知,QGIS是一个用户界面友好的桌面地理信息系统,可运行在Linux、Unix、Mac OSX和Windows等平台之上。 QGIS 基于 Qt 开发,除了提供可执行程序,还提供了一套用于二次开发的接口,可进行跨平台地理信息系统软件的定制开发。 正如所有的开发都是从安装编译器开始的,所有的二次开发都是从配置环境开始的。 然而,QGIS 的开发环境实属难配,如果要配置可调试的 Debug 环境更是难上加难。 经过了一段时间的开发之后,笔者总结了 QGIS 开发环境配置的基本过程,以及容易遇到的坑,在这里和大家分享。","text":"众所周知,QGIS是一个用户界面友好的桌面地理信息系统,可运行在Linux、Unix、Mac OSX和Windows等平台之上。 QGIS 基于 Qt 开发,除了提供可执行程序,还提供了一套用于二次开发的接口,可进行跨平台地理信息系统软件的定制开发。 正如所有的开发都是从安装编译器开始的,所有的二次开发都是从配置环境开始的。 然而,QGIS 的开发环境实属难配,如果要配置可调试的 Debug 环境更是难上加难。 经过了一段时间的开发之后,笔者总结了 QGIS 开发环境配置的基本过程,以及容易遇到的坑,在这里和大家分享。 Release 版开发环境配置Release 版开发环境配置相对比较简单,可以使用 OSGeo4W 直接下载各种预编译好的库。 除了下载比较慢以外,基本不会遇到其他问题。 使用 OSGeo4W 安装相关库在 OSGeo4W 官网下载安装程序后,即可进行安装。 安装方式选择 Advanced Install 安装源选择 Install from Internet ,当然如果是帮别人装可以选择 Download Without Installing ,如果用别人下载好的可以 Install from Local Directory 安装目录可以自己选 本地包下载路径可以自己选,安装完后就会删除 网络连接方式,如果没有梯子,就可以选择 Direct Connection ;有的话可以选择 Use IE5 Settings 直接导入系统代理配置,或者选择 Use HTTP/FTP Proxy 自己定义代理 然后会下载一些包,进入到下载点选择,可以直接选择 http://download.osgeo.org 那个。如果觉得网速比较慢,可以使用 GWmodel 实验室提供的 OSGeo4W 的镜像,在 User URL 里面输入 http://gwmodel.whu.edu.cn/mirrors/osgeo4w 然后点击 Add 按钮即可添加。 安装内容,可以在搜索框里面输入 qgis 进行搜索。可以看到有多种 qgis 版本安装包。综合各种因素考虑,最好是选择 qgis-rel-dev 这个版本的包,这个包是最新的 QGIS Release 版本的源代码(这里我之前安装了 qgis-dev)。点击 Skip 按钮选择版本号。OSGeo4W 会自动下载各种依赖包。 点击下一步后,要同意一些用户协议,之后即可进行安装。 Qt 安装为什么先说 OSGeo4W 安装再说 Qt 安装呢?因为 Qt 要安装的版本与 OSGeo4W 中安装的版本有关。 我们首先要查看已经安装了的 Qt 版本号,然后再安装对应的 Qt 开发工具。 当然,如果你不准备进行 Debug 环境的配置,那么其实可以只安装一个 Qt Creator 等相关工具。 开发的时候直接使用 OSGeo4W 安装的 Qt 库进行开发。 工程中引用工程引用其实非常简单,只需要在 pro 文件中加入 include 和 lib 的生命引用即可,此外再加一个 GDAL 的配置。 1234567## QGISINCLUDEPATH += \"$(OSGEO_HOME)/include\"INCLUDEPATH += \"$(OSGEO_HOME)/apps/qgis-rel-dev/include\"LIBS += -L\"$(OSGEO_HOME)/apps/qgis-rel-dev/lib\" -lqgis_core -lqgis_guiLIBS += -L\"$(OSGEO_HOME)/lib\" -lgdal_iGDAL_DATA = \".\\share\\gdal\"## QGIS END 这里面引用了一个 OSGEO_HOME 的环境变量,可以将这个变量设置到系统环境变量中,或者加入到工程环境变量中,变量值就是 OSGeo4W 的安装目录。 运行配置 该部分主要参考知乎文章 QtCreator进行QGis二次开发(1) ,有修改。 我们编译好自己的工程,运行的时候会提示缺 DLL 。常用的方法就是把需要的 DLL 放到工程生成的可执行程序目录下。主要需要这些 DLL :(为了描述方便,我直接写我的 Qt 工具的路径 C:/Qt/5.11.2/msvc2017_64 ) Qt 相关的 DLL 。可以使用 Qt 提供的 windeploy 工具直接部署。但是由于 QGIS 所引用的 DLL 用这个工具是无法部署过来的,因此还需要将缺少的 C:/Qt/5.11.2/msvc2017_64/bin 中的 DLL 拷贝过来(注意不需要以 d.dll 结尾的动态链接库)。如果嫌 windeploy 麻烦,可以直接将 C:/Qt/5.11.2/msvc2017_64/bin 的 DLL 拷贝过来,然后再把 C:/Qt/5.11.2/msvc2017_64/plugins 文件夹拷贝到工程生成的 exe 同级目录下(示例图中采用的是这种方法)。 OSGeo4W 安装的 DLL 。直接把 $(OSGEO_HOME)/bin 下的 DLL 拷过来。 QGIS 相关的 DLL 。直接把 $(OSGEO_HOME)/apps/qgis-rel-dev/bin 下的 DLL 拷过来。 QGIS 相关的 plugins 。直接把 $(OSGEO_HOME)/apps/qgis-rel-dev/plugins 下的 DLL 拷过来。 此外还有一些资源文件,主要有 $(OSGEO_HOME)/share/gdal 文件夹拷贝到工程生成的 exe 文件同级 share 目录下(如果没有就新建一个) $(OSGEO_HOME)/apps/qgis-rel-dev/resources 文件夹拷贝到工程生成的 exe 文件目录下。 此外还有一个运行时的环境变量 PROJ_LIB 需要配置,配置到 $(OSGEO_HOME)/apps/proj-dev/share/proj 目录即可。如果没有这个目录,就配置到 $(OSGEO_HOME)/share/proj 。这个主要是版本的问题。 如果后面开发中遇到了无法进行投影转换的问题,而且有 proj.db 相关数据库查询的保存,则很有可能就是这个 PROJ_LIB 变量的位置配错了,配成了不同版本的。因为 Proj 7.0 版本的数据库和 6.x 版本的数据库不一样。 这样我们的程序就可以运行了。 Debug 版环境的配置 事先声明:笔者编译出了 DEBUG 版可以执行的 qgis.exe ,但是不知道什么原因无法加载了 qgis_app.dll 因而无法运行。但是编译出来的库是可以在二次开发中使用的。 因为 OSGeo4W 只提供了 Release 版的库,如果要 Debug 版需要自己编译。整个过程没有那么复杂,但是容易遇到一些奇奇怪怪的问题。 首先需要下载 QGIS 最新 Release 版本的代码。你会发现最新 Release 的代码和 qgis-rel-dev 的版本是一致的,这就是之前选择 qgis-rel-dev 的原因。 然后用 Qt Creator 打开这个库的 CMakeList.txt 文件,就开始进行 CMake 工程的配置。 配置之前的准备配置之前要提前编译如下库的 Debug 版本的 lib 文件和 dll 文件: qca (非常重要,必须编译) qwt (最好编译) qtkeychain (最好编译,但理论上可以不编译) expat (可以不编译) 这些库基本都有 CMakeList.txt 文件,因此可以直接使用 Qt Creator 打开编译。但是如果用 CMake GUI 配置好生成 VS 解决方案,用 VS 打开进行编译,你会发现有一个 BUILD_ALL 和 INSTALL 项目,编译好后直接生成这个项目,就可以安装到 CMake 配置时 INSTALL_PREFIX 变量指定的目录。 理论上,Debug 程序是可以调用 Release 版本的库的,但是在 GUI 环境下,混用 Debug 和 Release 的库会导致程序崩溃。对于 QCA ,如果使用了 Release 版本的 QCA 库,那么程序运行的时候在初始化 QCA 的时候就会发生崩溃。所以 QCA 必须编译 DEBUG 版本。qwt 和 qtkeychain 最好也编译一下,毕竟也不麻烦。但是 qtkeychain 好像并不涉及 GUI ,如果嫌麻烦可以不编译。expat 库纯粹是因为其 release 和 debug 版本的库文件名不一样,可能 DEBUG 和 RELEASE 有区别,因此我编译了一下,但是直接用 OSGeo4W 的应该也是可以的。 至于如何编译这些库,网上有其他教程,这是个体力活,这里就不赘述了。 还要安装 Cygwin ,并且安装如下两个工具。安装完记得把 Cygwin/bin 目录添加到 PATH 环境变量中。 bison flex 还需要安装 ninja.exe 并拷贝到 $(OSGEO_HOME)/bin (其实理论上放到系统 PATH 环境变量包含的目录中都可以)。 编译配置打开 QGIS 的 CMakeLists.txt 文件,会有许多东西需要配置,因为所有的依赖库都要指定其 INCLUDE_DIR 和 LIBRARY 。用 OSGeo4W 安装的库都可以这样指定 自己编译的库也用类似的方式指定就可以了。 选择 WITH_QTWEBKIT 最好去掉,因为在 Qt 5.5 以后的版本中已经去掉了 QtWebKit 组件。虽然下个版本会加回来。选择 WITH_BINDINGS(Python 相关模块)、WITH_SERVER、WITH_POSTGRE 如果不需要相关的功能就可以去掉,如果勾选的话就需要相关的库。 选项 WITH_INTERNEL_MDAL 可以勾上,让 QGIS 自己编译 MDAL,当然就需要指定 HDF5、NetCDF 等库的位置,如果没有这些库可以从 OSGeo4W 中安装。如果关闭,那就自己编译 MDAL,然后提供 MDAL_INCLUDE_DIR 和 MDAL_LIBRARY 。 还要注意,如果勾选了 WITH_INTERNEL_MDAL 要给 CMAKE_CXX_FLAGS 开头的几个变量后面要添加 /D H5_BUILT_AS_DYNAMIC_LIB" 选项,否则无法编译。另外,需要在这几个变量中加上 /utf-8 选项,否则会报告“常量中有换行符”等错误。 然后代码基本就可以编译了。如果 CMAKE 还是出错,就采用以下三种方式解决: 从 OSGeo4W 中下载相应的库文件 自己编译以提供相应的库文件 推荐做法:关闭相关的 WITH 选项 编译后如果使用 VS 编译的,那么有 INSTALL 工程可以直接安装。如果是 QT 编译的,那么就手动把 dll 和 lib 等库拷贝出来吧, include 文件可以使用 qgis-rel-dev 的。还有 resource 文件夹别忘了。 至此,所有 QGIS 开发的环境配置已经完成了。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"Qt","slug":"Qt","permalink":"http://hpdell.github.io/tags/Qt/"},{"name":"QGIS","slug":"QGIS","permalink":"http://hpdell.github.io/tags/QGIS/"}]},{"title":"测试使用 StackEdit 更新博客","slug":"test-stackedit","date":"2020-01-09T22:21:30.000Z","updated":"2022-04-14T16:50:55.573Z","comments":true,"path":"随笔/test-stackedit/","link":"","permalink":"http://hpdell.github.io/随笔/test-stackedit/","excerpt":"","text":"这篇博客内容是通过 StackEdit 编写的,只是尝试一下使用这个工具编写然后同步,触发 Travis 持续集成,然后更新博客的流程。 Written with StackEdit.","categories":[{"name":"随笔","slug":"随笔","permalink":"http://hpdell.github.io/categories/随笔/"}],"tags":[]},{"title":"探索性因子分析","slug":"exploratory-factor-analysis","date":"2019-09-03T19:58:46.000Z","updated":"2022-04-14T16:50:55.433Z","comments":true,"path":"理论/exploratory-factor-analysis/","link":"","permalink":"http://hpdell.github.io/理论/exploratory-factor-analysis/","excerpt":"","text":"简介在之前的章节中,我们讨论了主成分分析——一种多元数据降维以及理解变量间关联模式的方法。 在这个章节中,我们讨论,这是一种类似的方法, 基于一种不同的隐含的模型,被称作””。 虽然这两种方法比较类似,但探索性因子分析经常被用于完成和主成分分析相同的目标。 我们这里的目的是强调他们之间概念上的区别。 公因子模型关于数据集中的每个变量是如何计量的有明确的假设。 模型认为每次计量的变化是由几个相对较少的共同因子造成的(即两个或多个变量之间不可观测的共同特性)。 尽管一个变量实际上可能有一个或多个特性因子,统计上不可能将这些特殊变量混淆。 探索性因子分析的目标是发现共同因子(区别于特性因子)并解释它们和观测数据之间的关系。 因此,尽管用于完成探索性因子分析的解决方案和主成分分析类似,但他们背后的模型是不相同的。 在探索性因子分析中,我们让观测到的数据间关联模式决定因子构成。 在《验证性因子分析》中,我们转向验证性数据分析, 利用一些关于因子构成的先验知识,去检验他们是否与数据一致。 在验证性数据分析中,背后的模型与探索性因子分析是相同的,但是求解过程则大不相同。 公因子模型提供了一个明确的框架让我们可以对数据计量属性进行评估。 在这个章节提供的案例中,我们假设特性因子的变化可以单纯地由计量误差解释。 广义地,为了区分特性方差(即特性因子的方差)和误差方差(即每个变量计量误差的方差),计量可靠性的独立评估是很必要的。 如果没有这种独立评估,我们就假设特性因子方差反映了计量误差。 这让我们对计量的可靠性有了一定的了解:误差方差越小,计量越可靠。 《验证性因子分析》中有关于可靠性的形式化概念。 在这章节的编写过程中,我们特别注重使用旋转来提高因子分析解的解释性。 因为探索性因子分析的方向是随意的,有时候选择有简单结构的解才是有意义的 (即在某种意义上最易于解释的一种,由 Thurstone 在1947年提出)。 实际上,在对数据进行主成分分析时旋转也同样可行。 这里所介绍的所有旋转方法都可以应用于主成分分析。 潜在应用探索性因子分析与大多数主成分分析的应用场景相同;而公因子模型当测量模型的明确假设是适当时更合适。 下面介绍探索性因子分析的两种说明性应用:分析”潜在特征”或”不可观测的特性”、使用因子得分分析依赖性。 分析”潜在特征”或”不可观测的特性”有的时候区分数据变量和它被设计用于计量的概念是非常重要的。 在处理物理特性时,例如长度和重量(测量仪器具有高精度),这种区分是不必要的,因为目标属性几乎可以被完美计量。 但是,当处理态度、信仰、感知以及其他心理学特性时,我们的计量方法是不完美的。 在市场,调查研究员可能对获取某个概念(例如”顾客满意度”)的信息感兴趣,以更好理解这个概念以及企业行为对它的影响。 设计一个单一的调查问题来准确认知如”顾客满意度”这种概念,即使是可能的,也非常困难。 相反的,研究者可能回设计一个包含几个问题的调查问卷,每个问题并不能完美反映”顾客满意度”,但能够反应某一个方面。 使用因子分析,可以找到这些问题背后的共同变量(很可能反映了客户潜在的满意度)并分离出计量中的非系统误差。 从公因子模型中得到的因子得分可以作为后续分析和建模中顾客满意度的一个指数(或多个指数,取决于潜在变量的个数)。 Aaker(1997)使用探索性因子分析研究了品牌特性的几个维度。 为了处理 114 个特性指标(从心理学和市场调研研究中产生的 309 个候选指标中筛选得到的), Aaker 让受访者对一组10个品牌中的每一个进行 114 个特性指标的 5 级尺度打分(从 1 到 5 ,1 代表完全没有体现,5 代表非常能够体现)。 她使用了四个不同的品牌数据集,每组都有一个焦点品牌(李维斯牛仔)和九个其他的 (都是非常突出的、广为人知的国民品牌,涵盖了多个不同的产品类型),一共 37 个不同的品牌。 Aaker 对打分结果按人数平均(每个品牌在每个指标上都被大约 150 到 160 个受访者打分,除了李维斯被所有人打分) 并对指标间 $114 \\times 114$ 维的相关矩阵进行了因子分析。 Aaker 选择了一个五个因子的解,解释了 90% 的指标差异。 在进行了旋转之后,她将这些因子标记为: 诚意(sincerity,解释了 26.5% 的差异)、刺激(excitement,25.1%)、竞争力(competence,17.5%)、 世故(sophistication,11.9%)和粗犷(ruggedness,8.8%)。 表展示了这五个因子的一些最高度相关的指标。 基于这个研究的结果, Aaker 继续构造并验证了 42 项测度用于测量者五个品牌特性的成分。 Sincerity Excitement Competence Sophistication Ruggedness Honest Daring Reliable Glamorous Tough Genuine Spirited Responsible Pretentious Strong Cheerful Imaginative Dependable Charming Outdoorsy Down-to-earth Up-to-date Efficient Romantic Masculine Friendly Cool Intelligent Upper class Successful Smooth 使用因子得分分析依赖性和主成分分析一样,减少维数往往是因子分析的主要目标。 一方面,这么做有助于对数据进行可视化;另一方面,也有助于增加模型简洁性。 例如,在一个关于新的豪华车概念的大型市场调研中,Roberts(1984)调查了 162 个目标消费者关于他们在汽车的认知。 他使用九个维度来计量他们在熟悉的汽车模型的认值:奢华、样式、可靠性、油耗、安全、维修成本、质量、续航以及道路偏好。 他的最终目标是建立一个模型,将认知和偏好联系起来;但是可用于拟合这个模型的自由维度数太有限。 因此他使用因子分析来看看他是否可以找到一组更少数目的潜在公因子可以替代。 Roberts 发现一个双因子解解释了这九个特性中 60% 的差异。Roberts 然后旋转了解(使用方差最大法)来使得它更容易解释。 指标和因子之间的相关系数被称为, 如下表所示;相关系数最高的是用下划线标出。 这种模式指示第一个因子(与奢华、样式、安全性、道路偏好等指标高度相关)反映了车的情感诉求, 而第二个指标(与可靠性、油耗、维修成本、质量和续航相关)反映了车的经济性。 Roberts 将这两个因子标记为吸引力和合理性。 Appealing Sensible Luxury 0.884 -0.051 Style 0.748 0.153 Reliability 0.396 0.691 Fuel Economy -0.202 0.786 Safety 0.720 0.172 Maintenance 0.149 0.756 Quality 0.501 0.650 Durable 0.386 0.677 Performance 0.686 0.391 使用这个模型, Roberts 计算每个被打分的车模在每个因子得分的平均值,然后使用这些因子得分来回归前面所述的受访者喜好。 使用这两个因子,他能够解释 30% 的受访者喜好的差异(依据调整 );这两个因子都高度显著。 相反,当 Roberts 使用 9 个指标来回归受访者喜好时,拟合数据的能力仅有轻微的提高(调整 是 33%), 而他参数估计值的标准差大幅增加,由于指标间的多重共线性(实际上,九个系数中只有两个在 0.05 等级下显著)。 使用这个简洁的因子模型, Roberts 能够评估新概念车的相对定位,并精确预测市场价值。 原理直观认识和主成分分析一样,对因子分析的直观感觉也最好是通过一个简单的例子进行。 考虑 Holzinger 和 Swineford 进行的针对儿童的心理学考试(1939)。 他们对七、八年级的儿童进行了一些不同的考试。 为了使问题简化,我们主要关注下面五个考试: 篇章理解($PARA$)、句子完成($SENT$)、单词含义($WORD$)、加法($ADD$)、数点($DOTS$)。 我们使用变量 $X_1$到 $X_5$表示这五个不同的考试。 他们间的相关系数(基于 145 个儿童组成的样本)如下表所示。 $PARA$ $SENT$ $WORD$ $ADD$ $DOTS$ $PARA$ $1.000$ $SENT$ $0.722$ $1.000$ $WORD$ $0.714$ $0.685$ $1.000$ $ADD $ $0.203$ $0.246$ $0.170$ $1.000$ $DOTS$ $0.095$ $0.181$ $0.113$ $0.585$ $1.000$ 在因子分析中,我们假设考试得分可以用一个潜在公因子和几个特殊因子(每个考试一个特殊因子)的函数来描述。 也就是说,例如,我们相信这些学生的考试得分背后都有一个公因子,记为 $\\xi$ 。 这个因子可能反映了每个学生的智力或应试能力。 我们的但因子模型表示,对考试 $i$的得分 $X_i$是因子 $\\xi$ (对所有五个考试来说都是相同的) 和考试 $i$的特有的因子——不妨记为 $\\delta_i$——的函数。 在很多可以用于估计公因子模型参数的方法中,所有我们实际可以估计的是唯一方差(或称”唯一性”),即这个特殊因子和测量误差方差的和。 我们假设这个特殊因子方差完全由测量误差引起,反映考试在完美捕获背后共同因子能力的不足。 由于特殊因子仅影响他们特别针对的计量(即 $\\delta_i$仅影响考试 $i$), 我们的后续处理将在假设这些特殊因子 $\\delta$互不相关 (即对于所有不同 [^1] 的 $i$和 $j$,$\\delta_i$和 $\\delta_j$之间的相关系数是 0)。 且与潜在的公因子 $\\xi$也不相关的假设下进行。 这些都是公因子模型的标准假设。 这个单因子模型可以用图中的示意图所表示。 图中指向每个考试变量的箭头(即观测计量值,用盒形表示)指示测量中变异的贡献源。 这种情况下,每个测量值 $X_i$有两个变异的贡献源(都不可观测):公因子 $\\xi$和一个特殊因子 $\\delta_i$。 用公式的形式,可以写作下式 $$\\begin{aligned} X_1 & = \\lambda_1\\xi + \\delta_1 \\\\ X_2 & = \\lambda_2\\xi + \\delta_2 \\\\ X_3 & = \\lambda_3\\xi + \\delta_3 \\\\ X_4 & = \\lambda_4\\xi + \\delta_4 \\\\ X_5 & = \\lambda_5\\xi + \\delta_5 \\\\ \\end{aligned}$$ 在这些公式中,系数 $\\lambda$ 反映了潜在公因子 $\\xi$体现在测量值 $X$中的程度。 假设 $X$和 $\\xi$ 都是标准化的变量(即有零均值和单位方差), $X_i$的方差可以被分解为: $$\\mathrm{var}{X_i} = \\mathrm{var}{\\lambda_i\\xi + \\delta_i} = \\lambda_i^2 + \\mathrm{var}{\\delta_i} = 1$$ 由于变量被标准化,$\\lambda_i$可以解释为相关系数。 $\\lambda_i^2$ 可被解释为 $X_i$中的变化在公因子 $\\xi$反映的比例,被称作 $X_i$的。 $X_i$中剩余的变化可由特殊因子 $\\delta_i$解释。 如果用 $\\theta_{ii} = \\mathrm{var}{\\delta_i}$来表示特殊因子的方差(我们假设它反映了 $X_i$的计量误差), 那么 $X_i$的共同度等于 $$1 - \\theta_{ii}^2$$ 如果 $X_i$的共同度接近 1 (即误差方差接近 0),表示 $X_i$是潜在公因子 $\\xi$的一个几乎完美的度量。 反之,如果共同度接近 0,表示 $\\xi$ 完全没有体现在 $X_i$中 (如果 $X_i$ 是一个对学生智力设计较差的考试,这种情况就可能发生), 那么系数 $\\lambda$可能接近零,并且几乎 $X_i$的所有方差都由特殊因子 $\\delta_i$ 解释。 在这个特殊的示例中, 我们从一个潜在于 Holzinger 和 Swineford 的研究中考试得分的单共同因子假设开始(一种类似于通用智力的东西)。 但是,也有一种情况,即考试中的个人表现可能是多于一个潜在能力的函数。 例如,我们可能认为学生的考试表现有两个不同的内在能力所决定—— 文学天赋(记为 $\\xi_1$)和计量天赋(记为 $\\xi_2$)—— 并且这两种不同的因子在不同类型的考试中有不同程度的表现。 这个双因子模型如图所示。 在双因子模型中,在每个考试得分度量 $X_i$中有三个源贡献了变异: 两个公因子 $\\xi_1$和 $\\xi_2$以及一个特殊变量 $\\delta_i$。 使用公式的形式,双因子模型可以写作: $$\\begin{aligned} X_1 & = \\lambda_{11}\\xi_1 + \\lambda_{12}\\xi_2 + \\delta_1 \\\\ X_2 & = \\lambda_{21}\\xi_1 + \\lambda_{22}\\xi_2 + \\delta_2 \\\\ X_3 & = \\lambda_{31}\\xi_1 + \\lambda_{32}\\xi_2 + \\delta_3 \\\\ X_4 & = \\lambda_{41}\\xi_1 + \\lambda_{42}\\xi_2 + \\delta_4 \\\\ X_5 & = \\lambda_{51}\\xi_1 + \\lambda_{52}\\xi_2 + \\delta_5 \\\\ \\end{aligned}$$ 如前,系数 $\\lambda$反映了潜在公因子 $\\xi$体现在测量值 $X$中的程度。 如果 $X$和 $\\xi$被标准化,参数 $\\lambda$可以被解释为相关系数。 如果我们进一步假设潜在因子互不相关(公因子模型的另一个标准假设), 那么每个考试度量的共同度由那个变量因子载荷的平方和给出; 因此,$X_i$ 的共同度由 $\\lambda_{11}^2 + \\lambda_{12}^2$给出。 考虑在双因子模型下,如果一个学生有较高的文学天赋(高 $\\xi_1$)和较低的计量天赋(低 $\\xi_2$),那会发生什么。 我们可以预期这个学生在那些比计量天赋更需要文学天赋的考试中表现良好。 如果学生在句子完成考试中的表现只依赖于他(她)的文学天赋,那么我们应该期望一个 接近于 1 的 $\\lambda_{11}$的值和一个接近于 0 的 $\\lambda_{12}$的值。 在没有较强的关于学生考试表现潜在因子结构的先验知识这个探索性设定中, 我们希望能够从数据中能够推到出潜在因子的合理数量以及公因子模型公式中的系数值。 这个方法(在下一节中会详细阐述)和主成分分析的方法相类似,都是尝试提取一个较少数量的因子组以充分代表观测值的相关矩阵。 不同的是,公因子模型中,我们必须解释特殊因子 $\\delta$ ,而这没有在主成分分析中体现。 求解过程和主成分分析一样,探索性因子分析的求解过程主要关注于 $\\mathbf{X}$ 的协方差矩阵或相关系数矩阵的分解。 这两种方法的不同之处在于,特殊因子是公因子模型的一部分。 由于这些特殊因子被认为彼此不相关,而且与潜在公因子独立,这些因子仅仅贡献于协方差矩阵的对角线元素。 这一点通过检查每个度量值 $X_i$协方差矩阵的对角线元素可以很容易发现: $$\\mathrm{var}{X_i} = \\mathrm{var}{\\lambda_{i1}\\xi_1 + \\lambda_{i2}\\xi_2 + \\delta_i}$$ $X_i$方差表达式中有九个交叉项。 根据公因子模型加入的独立性假设(以及进一步的公因子被标准化为具有单位方差的假设), 所有的协方差项都排除在模型之外,只剩 $$\\mathrm{var}{X_i} = \\lambda_{i1}^2 + \\lambda_{i2}^2 + \\theta_{ii}^2$$ 其中 $\\theta_{ii}^2 = \\mathrm{var}{\\delta_i}$。 因此,特殊因子出现在 $\\mathbf{X}$ 的协方差矩阵中的唯一位置就是对角线,也就是他们对每个度量贡献变异的地方。 如果我们实现直到每个特殊因子的变异呢? 如果这样,那么我们可以在协方差矩阵中减去这些值。 然后我们剩下一个矩阵,仅由潜在公因子的方差和协方差构成。 然后我们可以使用主成分分析方法来分解这些矩阵,并寻找公因子。 这是我们在探索性因子分析中使用的求解过程的本质。 在主成分分析中,我们分解相关矩阵 $\\mathbf{R}$(对角线元素都是 1)。 但是对于公因子模型,我们分解对角线元素为 $1 - \\theta_{ii}^2$ 的相关矩阵。 实际上,我们通过在模型中减去由于特殊因子引起的变异开始 (回想一下,这些变异可以被解释为测量误差或其他针对于不同考试的变异源,而且和其他因子不相关), 仅在对角线上保留共同度。 然后我们尝试使用公因子解释剩下的变异。 如果我们知道共同度,那么我们就可以使用与主成分分析相同的方法解决这个问题。 这有时被称为(或简单的被称为公因子模型的主成分方法)。 共同度的估计是这个求解过程中非平凡的部分,有许多不同的方法。 现在让我们假设我们不知道在我们这个解释性示例中由于特殊因子引起的变异的大小 (在后续章节中,我们讨论当我们不知道这些值时该怎么办)。 对于这个考试集合,我们假设每个考试中大约一半的变异都有特殊因子产生, 也就是说,我们开始时对所有 $i$设置 $\\theta_{ii}^2 = 0.50$ 然后对这个修改的矩阵进行矩阵分解。 特征值如下所示: $$\\begin{array}{ccccc} \\lambda_1 = 2.187 & \\lambda_2 = 1.022 & \\lambda_3 = -0.135 & \\lambda_4 = -0.089 & \\lambda_5 = 0.015 \\end{array}$$ 首先值得注意的是,这些特征值和主成分分析的特征值有所不同。 并不是所有的特征值都是正的,并且他们的和也不是 $p$ (分析中变量的数目)。 原因是,通过减去由于特殊因子引起的变异,我们减少了需要由公因子解释的剩余的信息。 这个模型中已经由特殊因子解释的变异是 $0.50 + 0.50 + 0.50 + 0.50 + 0.50 = 2.5$, 即 $2.5/5.0 = 50\\%$ 的原始五个考试得分的变异。 对角线元素的和也是 $2.5$,即由公因子解释的方差。 现在仍然存在问题:我们需要多少公因子? 注意到选择提取的因子数量的标准与主成分分析相比多少有点不同变化。 由于有一些解释了一部分变异的特殊因子,我们的目标是使用公因子解释尽可能多的剩余变异。 因此我们寻找有意义地不同于零地特征值。 在这种情况下,是 2,表示我们提取 $c = 2$个公因子。 双因子解的矩阵(即有原始变量和公因子地相关系数组成地矩阵) 如表所示。 这种因子结构的模式表现出前三个考试(篇章理解、句子完成、单词含义)在第一个公因子上载荷更多(所有相关系数都接近0.8), 而后两个考试(加法和计数)在第二个公因子上载荷更高(所有相关系数都接近0.6)。 这和第一个因子反映学生的文学天赋、第二个因子指示计量天赋的解释一致。 Factor 1 Factor 2 $PARA$ $0.7722$ $-0.2351$ $SENT$ $0.7838$ $-0.1576$ $WORD$ $0.7562$ $-0.2372$ $ADD$ $0.4293$ $0.6017$ $DOTS$ $0.3476$ $0.6506$ 使用因子载荷,我们也可以通过这两个公因子计算每项考试解释的变异所占的比例。 这个比例被称为。 例如,对于篇章理解考试($X_1$),公因子解释了考试中 $(0.77)^2 + (-0.24)^2 = 0.65$即 65% 的变异。 这个共同度值比我们一开始在分析中使用的 $1 - \\theta_{11}^2 = 0.50$ 的值略高。 这是因为我们的起点是基于一些先验知识,只是近似的。 对于学生的特定样本,精确值更可能是不同的值。 通过跌多过程来修正我们初始估计值是可行的,最终我们会得到一个一致的结果。 我们用第一次因子分析的结果替代我们处是共同度估计值,并再次进行因子分析。 那么,我们将使用新的值 0.65 代替 $X_1$(篇章理解) 对应的对角线上的元素 0.50, 对于其他变量我们也这么做。 我们可以继续一次迭代、两次迭代这个过程,直到它收敛——也就是说,直到两次迭代之间共同度的变化足够小。 这个迭代过程经常被用于共同度的估计。 这个方法有时被称为。 如果我们没有足够的关于我们数据中测量误差的先验知识(即我们的测量中与潜在因子无关的非系统变异)呢? 我们使用什么来设置共同度的初始估计值? 一个被广泛使用的方法是, 即在数据集中一个变量中可被其他所有变量解释的方差的大小。 例如,如果我们想使用多元相关平方系数作为作为 $X_1$ (篇章理解)的初始估值, 我们将 $X_1$与其余的变量 $X_2, X_3, X_4, X_5$ 进行回归,并使用 值。 为什么多元相关平方系数是共同度的良好估值? 因为共同度是由公因子 $\\xi$ 所解释的 $X$中方差的占比。 尽管我们因此喜欢用公因子 $\\xi$对 $X$ 进行回归,但也仍然存在公因子不可被观测的问题。 但是我们有其余变量 $X$,每个都反映了潜在变量 $\\xi$(尽管并不完美)。 因为测量有误差,他们解释 $X$的能力被减弱了。 因此,多元相关平方系数可以作为共同度的下界。 一般地,测量越可靠(即特殊因子有较小的方差),多元相关平方系数作为共同度的估值越准确。 表展示了考试得分数据使用多元相关平方系数作为共同度估值的因子分析结果。 我们使用一个像上述的迭代过程:用修正的共同度估值代替初始值并继续,直到后续的共同度估计值没有明显改变。 表所示的初始的因子载荷和 表所示的最终结果的差异并不显著。 因子模式指向相同的因子本质的解释。 变异的划分也基本相同:由共同因子解释的变异的大小仍然大约是 66%。 对比两个初始估计值,最后两个考试(加法和计数)相关的共同度比前三个低: 对于 $X_4$和 $X_5$,共同度大约是 0.60,而对于 $X_1$,$X_2$和 $X_3$ 共同度大约是 0.70。 如果我们认为这种差异来自于测量误差,那么我们可能得出后两个考试比前两个考试更不可靠的结论。 关于这种共同度的估计方法,需要注意一点。 如果技术考试被省略,仅保留加法作为学生计量天赋的唯一度量,会怎么样? 在这些情况下,基于多元相关平方系数计算的假发考试的共同度的估值会非常低 (因为前三个考试没有任何一个反映第二个共同因子)。 因此,我们更可能得出加法是一项非常不可靠的考试,但实际上它是一个重要潜在结构的唯一度量。 正是共同度估值的这个问题,导致有人更喜欢主成分分析法而不是因子分析法。 旋转因子解在下面第严密推导节中我们会看到,公因子模型实际上拥有无穷多解, 每种解都有平等的能力再现观测到的协方差矩阵。 原因是因子解的方向(即描述坐标系统的基向量的选择)是非常随意的。 这被称为公因子模型的。 在主成分分析中,为了避免不确定性,我们将问题这样定义: 第一主成分是原始数据(经过合理缩放)具有最大方差的线性组合, 第二主成分是拥有次大方差(条件是与第一主成分不相关)的线性组合,等等。 这保证了唯一(尽管有些随意)解。 如果因子解的方向是非常随意的,那么为什么不选择一个可以更好地帮助我们理解并解释数据的解? 使用因子载荷矩阵尝试提出潜在公因子的一个明确解释往往是比较头疼的。 如果我们可以通过旋转因子解选择一个不同的方向使载荷矩阵得到简化则是非常有利的。 问题就是,如何选择这个方向? 如何使变化因子载荷矩阵的目标变得可操作,使我们可以找到让我们更接近那个目标的因子解的旋转方式? 寻找这个旋转矩阵 $\\mathbf{T}$(我们暂时将讨论限制在正交旋转矩阵范围内,这保留了潜在公因子的独立性)最流行的方法 是基于 Thurstone (1947)描述简单结构原则。 Thurstone 认为大多数内容域可能涉及几个隐藏的(即潜在的或不可观测的)因子。 他同样假设任何一个观测到的变量可能和一个或几个潜在因子相联系,而且任何一个因子可能仅和几个变量相联系。 那么最一般的想法,只要可能,就是找到几个变量簇,每个簇仅明确一个因子。 更一般地,我们想要找到几个因子轴的方向,使得每项考试(或其他变量)尽在几个因子上有相对较高的载荷(正或负), 而其他因子的载荷趋于零。 如果潜在简单结构的论证有效,那么因子载荷矩阵应该展现出一种特定的模式(Comery,1973): 大多数特殊因子(列)的载荷应当很小(尽可能接近于零),仅仅一些载荷的绝对值应该很大。 载荷矩阵的某行,包括每个因子的给定变量的载荷,应当在一个或少数因子上表现出非零载荷。 任何一对因子(列)应当展现出不同的载荷模式。否则,这两列所表示的因子无法区分。 一个可以表明简单结构概念的假设例子如表和图所示。 设想一个对消费者对止痛药的看法的研究,其中要求每个受试者根据六个属性评价他(她)的首选止痛药品牌: 不反胃 没有副作用 能止痛 起效快 能使人保持清醒 适度依赖性[^2] 在表中第一部分所示的因子载荷矩阵是未经旋转的双因子分析的解。 (回想一下,这些未旋转的因子通过使第一个公因子解释最多变异、 第二个公因子在与第一个公因子物馆的条件下尽可能解释更多剩下的变异而定向。) 注意到所有载荷矩阵的 12 个元素都相对较大(绝对值都大于 0.4)。 有这么多重大交叉载荷,获得因子的简单解释就变得比较困难。 未旋转解 旋转的解 特性 Factor 1 Factor 2 Factor 1 Factor 2 不反胃 $0.579$ $-0.452$ $0.139$ $0.721$ 没有副作用 $0.522$ $-0.572$ $0.017$ $0.774$ 能止痛 $0.645$ $0.436$ $0.772$ $0.097$ 起效快 $0.542$ $0.542$ $0.764$ $-0.051$ 能使人保持清醒 $-0.476$ $0.596$ $-0.034$ $-0.762$ 适度依赖性 $-0.613$ $-0.439$ $-0.750$ $-0.074$ 解释的方差 解释的方差 $1.921$ $1.562$ $1.765$ $1.718$ 图绘制出因子载荷, 并展示旋转因子空间使特性更接近地位于公因子附近如何成为可能。 想法是选择一个旋转角,使每个特性在公因子上地投影要么很大(即绝对值将近1)要么很小(绝对值接近 0)。 这实际上减小了交叉载荷并在简单结构方向上移动了载荷矩阵。 表所示的旋转的因子载荷矩阵展示了接近简单结构的东西。 特性 3 和 4(”能止痛”和”起效快”)在第一个公因子上有正载荷,特性 6(适度依赖性)有负载荷。 特性 1 和 2(”不反胃”和”没有副作用”)在第二个公因子上有正载荷, 特性 5(”能保持清醒”)有负载荷。 所有其他的载荷的绝对值都很小。 给定定义每个因子的属性簇,我们可以标记第一个因子为”有效性”和第二个因子”温和性”。 (负载荷反映负相关。例如,”能保持清醒”的得分越高,”温和性”得分越低。) 需要注意的是,旋转过后,初始因子方向方差最大化的性质就丢失了; 也就是说,虽然总体上保留的因子能够解释与原先的数据集中一样多的变异, 但这种变异现在在旋转配置的新维度上有所不同。 因此,第一个(旋转后的)因子解释最大的变异的情况不存在了。 由于每个因子解释的变异的大小通常并不是关心的主要目标, 如果旋转后的解更容易解释,那么这种取舍通常认为是值得的。 严密推导在这个更形式化的因子分析模型的处理中,回到主成分分析模型的建立过程中是非常有用的。 我们已经介绍了,对主成分分析问题的求解等同于对标准化的数据矩阵 $\\mathbf{X}$ 进行奇异值分解,如下所示: $$\\mathbf{X} = \\mathbf{Z}{\\mathrm{s}} \\mathbf{D}^{\\frac{1}{2}}\\mathbf{U}^{\\mathrm{T}}$$ 其中 $\\mathbf{Z}{\\mathrm{s}}$是标准化的主成分(全都互不相关), $\\mathbf{D}^{\\frac{1}{2}}$是对角线元素全为主成分标准差的对角矩阵, $\\mathbf{U}$ 是特征向量矩阵(全都互相正交)。 据此,我们可以将样本相关矩阵 $\\mathbf{R}$ 重写为奇异值分解得到的特征值和特征向量的函数。 相关系数矩阵为 $$\\mathbf{R} = \\frac{1}{n-1} \\mathbf{X}^{\\mathrm{T}} \\mathbf{X}$$ 带入式中的奇异值分解结果到式中并简化可得 $$\\begin{aligned} \\mathbf{R} & = \\frac{1}{n-1}(\\mathbf{Z}{\\mathrm{s}} \\mathbf{D}^{\\frac{1}{2}}\\mathbf{U}^{\\mathrm{T}})^{\\mathrm{T}}(\\mathbf{Z}{\\mathrm{s}} \\mathbf{D}^{\\frac{1}{2}}\\mathbf{U}^{\\mathrm{T}}) \\\\ & = \\frac{1}{n-1}\\mathbf{U}\\mathbf{D}^{\\frac{1}{2}}(\\mathbf{Z}{\\mathrm{s}}^{\\mathrm{T}}\\mathbf{Z}{\\mathrm{s}})\\mathbf{D}^{\\frac{1}{2}}\\mathbf{U}^{\\mathrm{T}} \\\\ & = (\\mathbf{U}\\mathbf{D}^{\\frac{1}{2}})(\\mathbf{U}\\mathbf{D}^{\\frac{1}{2}})^{\\mathrm{T}} \\end{aligned}$$ 因为 $\\frac{1}{(n-1)}\\mathbf{Z}{\\mathrm{s}}^{\\mathrm{T}}\\mathbf{Z}{\\mathrm{s}}$正好是一个单位阵。 如果我们继续回忆,矩阵乘积 $\\mathbf{U}\\mathbf{D}^{\\frac{1}{2}}$正好是因子载荷矩阵 $\\mathbf{F}$(即原始数据矩阵 $\\mathbf{X}$和主成分矩阵 $\\mathbf{Z}$的相关系数矩阵), 我们可以 $\\mathbf{R}$的表达式简化为如下形式: $$\\mathbf{R} = \\mathbf{F}\\mathbf{F}^{\\mathrm{T}}$$ 由于我们使用主成分分析的目标往往是降维,所以我们尝试提取由 $c$ 个成分组成的子集 (其中 $c < p$,$p$是 $\\mathbf{X}$中变量的个数)以近似 $\\mathbf{R}$。 因此,在主成分中有 $$\\mathbf{R} \\approx \\mathbf{F}_c\\mathbf{F}_c^{\\mathrm{T}}$$ 其中 $\\mathbf{F}_c$仅由因子载荷矩阵 $\\mathbf{F}$ 的前几列组成。 在探索性因子分析中,我们同样尝试近似相关矩阵 $\\mathbf{R}$,但是使用一个不同的模型。 在式中不是将 $\\mathbf{X}$ 的奇异值分解代入,而是使用上述的公因子模型。 有 $c$ 个公因子的一般化模型写作: $$\\begin{aligned} X_1 & = \\lambda_{11}\\xi_{1} + \\lambda_{12}\\xi_{2} + \\cdots + \\lambda_{1c}\\xi_{c} + \\delta_1 \\\\ X_2 & = \\lambda_{21}\\xi_{1} + \\lambda_{22}\\xi_{2} + \\cdots + \\lambda_{2c}\\xi_{c} + \\delta_2 \\\\ X_3 & = \\lambda_{31}\\xi_{1} + \\lambda_{32}\\xi_{2} + \\cdots + \\lambda_{3c}\\xi_{c} + \\delta_3 \\\\ & \\vdots \\\\ X_p & = \\lambda_{p1}\\xi_{1} + \\lambda_{p2}\\xi_{2} + \\cdots + \\lambda_{pc}\\xi_{c} + \\delta_p \\\\ \\end{aligned}$$ 使用矩阵的形式,公因子模型表示为: $$\\begin{aligned} \\mathbf{X} = \\mathbf{\\Xi}\\mathbf{\\Lambda}_c^{\\mathrm{T}} + \\mathbf{\\Delta} \\end{aligned}$$ 其中 $\\mathbf{\\Xi} = [\\xi_1, \\xi_2, \\cdots, \\xi_c]$,$\\mathbf{\\Delta} = [\\delta_1, \\delta_2, \\cdots, \\delta_n]$, $\\mathbf{\\Lambda}_c$是一个 $p \\times c$的系数矩阵。 另外,我们使用下面三个关于公因子模型成分的假设 公因子 $\\xi$互不相关,且有单位方差 $$\\frac{1}{n-1}\\mathbf{\\Xi}^{\\mathrm{T}}\\mathbf{\\Xi} = \\mathbf{I}$$ 特殊因子 $\\delta$互不相关,且拥有对角化协方差矩阵 $$\\mathbf{\\Theta} = \\frac{1}{n-1}\\mathbf{\\Delta}^{\\mathrm{T}}\\mathbf{\\Delta} = \\mathrm{diag}(\\theta_{11}, \\theta_{22}, \\cdots, \\theta_{pp})$$ 公因子 $\\xi$和特殊因子 $\\delta$互不相关 $$\\mathbf{\\Xi}^{\\mathrm{T}}\\mathbf{\\Delta} = \\mathbf{0}$$ 现在我们将公式 $$\\mathbf{R} = \\frac{1}{n-1} \\mathbf{X}^{\\mathrm{T}} \\mathbf{X}$$ 中的 $\\mathbf{X}$ 用公式 $$\\mathbf{X} = \\mathbf{\\Xi}\\mathbf{\\Lambda}_c^{\\mathrm{T}} + \\mathbf{\\Delta}$$ 代替, 来对相关系数矩阵 $\\mathbf{R}$ 进行近似: $$\\begin{aligned} \\mathbf{R} & = \\frac{1}{n-1}(\\mathbf{\\Xi}\\mathbf{\\Lambda}_c^{\\mathrm{T}} + \\mathbf{\\Delta})^{\\mathrm{T}}(\\mathbf{\\Xi}\\mathbf{\\Lambda}_c^{\\mathrm{T}} + \\mathbf{\\Delta}) \\\\ & = \\frac{1}{n-1}( \\mathbf{\\Lambda}_c\\mathbf{\\Xi}^{\\mathrm{T}}\\mathbf{\\Xi}\\mathbf{\\Lambda}_c + \\mathbf{\\Delta}^{\\mathrm{T}}\\mathbf{\\Xi}\\mathbf{\\Lambda}_c^{\\mathrm{T}} + \\mathbf{\\Lambda}_c\\mathbf{\\Xi}^{\\mathrm{T}}\\mathbf{\\Delta} + \\mathbf{\\Delta}^{\\mathrm{T}}\\mathbf{\\Delta} ) \\end{aligned}$$ 基于公因子模型的假设 3,上式圆括号中的第二项和第三项为零。 基于假设 1,第一项中的表达式 $\\frac{1}{(n-1)}\\mathbf{\\Xi}^{\\mathrm{T}}\\mathbf{\\Xi}$可以替换为单位矩阵 $\\mathbf{I}$。 基于假设 2,最后一项的表达式变为 $\\mathbf{\\Theta}$。 通过这些简化,我们可以得到 $$\\mathbf{R} = \\mathbf{\\Lambda}_c\\mathbf{\\Lambda}_c^{\\mathrm{T}} + \\mathbf{\\Theta}$$ 或者 $$\\mathbf{R} - \\mathbf{\\Theta} = \\mathbf{\\Lambda}_c\\mathbf{\\Lambda}_c^{\\mathrm{T}}$$ 将主成分分析模型的公式 $$\\mathbf{R} \\approx \\mathbf{F}_c\\mathbf{F}_c^{\\mathrm{T}}$$ 和公因子模型的公式 $$\\mathbf{R} - \\mathbf{\\Theta} = \\mathbf{\\Lambda}_c\\mathbf{\\Lambda}_c^{\\mathrm{T}}$$ 进行对比,可以发现这两个方法之间的相似性以及本质区别。 两种情况下,我们都是对一个二次对称矩阵进行分解。 矩阵 $\\mathbf{\\Lambda}_c$ 事实上与矩阵 $\\mathbf{F}_c$同构: 是因子载荷矩阵,其元素可被解释为原始变量 $\\mathbf{X}$和提取的 $c$个公因子的相关系数。 旋转不确定性在主成分分析中,我们按顺序方式选择每个成分,以解释原始数据中尽可能大的变异,条件是与所有先前选定的主成分不相关。 这确保了一个唯一解,虽然在选择的方向上有点武断。 但是在公因子模型中,我们没有强加这种约束。 结果是,事实上有无穷多解,他们对矩阵 $\\mathbf{R} - \\mathbf{\\Theta}$ 能够近似的程度是相同的。 我们将这个属性称为公因子模型的。 我们首先通过一个例子来展示这种不确定性。 考虑表所示的考试的分数据的双因子解。 图展示了由该表中因子载荷矩阵所绘制的图表。 我们现在通过将他们顺时针旋转30°来改变因子的方向。 旋转(通过矩阵乘法进行)保留了两个因子的正交性。 由章节,我们知道进行正交旋转(二维)的矩阵具有以下形式: $$\\mathbf{T} = \\begin{pmatrix} \\cos\\alpha & -\\sin\\alpha \\\\ \\sin\\alpha & \\cos\\alpha \\end{pmatrix}$$ 当旋转角 $\\alpha = -30$ 度时,我们得到下列正交旋转矩阵 $\\mathbf{T}$ $$\\mathbf{T} = \\begin{pmatrix} 0.866 & 0.500 \\\\ -0.500 & 0.866 \\end{pmatrix}$$ 通过改变代表公因子的轴的方向,我们也改变了因子载荷的值。 我们可以计算新的载荷(记为 $\\mathbf{\\Lambda}_c^$),正好是旋转后的因子 $\\mathbf{\\Xi}\\mathbf{T}$和原始变量 $\\mathbf{X}$(由 $\\mathbf{\\Xi}\\mathbf{\\Lambda}_c^{\\mathrm{T}} + \\mathbf{\\Delta}$ 给出)的相关系数,简化为 $$\\mathbf{\\Lambda}_c^ = \\frac{1}{n-1}(\\mathbf{\\Xi}\\mathbf{\\Lambda}_c^{\\mathrm{T}}+\\mathbf{\\Delta})^{\\mathrm{T}}\\mathbf{\\Xi}\\mathbf{T}$$ 由于 $\\frac{1}{(n-1)}\\mathbf{\\Xi}^{\\mathrm{T}}\\mathbf{\\Xi} = \\mathbf{I}$且 $\\mathbf{\\Delta}^{\\mathrm{T}}\\mathbf{\\Xi} = 0$。 旋转后的因子载荷 $\\mathbf{\\Lambda}_c^*$ 如表中原始未旋转载荷的旁边所示。 正如之前所说的,矩阵中的主要载荷(即那些拥有高绝对值的载荷)并没有发生显著改变: 前三项考试在第一个因子的载荷和后两个在第二个因子的载荷。 但是注意旋转后的解的交叉载荷改变了。 前三项考试现在在第二个因子上由正载荷(而不是负载荷), 而后两项考试则几乎完全负载在第二个因子上(而不是在两个因子上都有正载荷)。 关于旋转因子解有两个重要的性质。 第一,尽管每个因子解释的方差发生了变化,两个因子解释的总方差仍然相同。 因为未旋转的解由分解矩阵 $\\mathbf{R} - \\mathbf{\\Theta}$获得, 这个解的方向使第一个因子能够解释最多的变异、第二个因子能够解释剩下的变异。 旋转改变了方向,使得由第一个因子解释的变异变小了。 但是旋转仅改变公因子空间轴的方向,所以并不影响解释的总方差。 第二个旋转解的性质使共同度没有改变。通过从旋转的因子载荷重构矩阵 $\\mathbf{R} - \\mathbf{\\Theta}$可以很容易发现这一性质。 我们有 $$\\begin{aligned} \\mathbf{R} - \\mathbf{\\Theta} & = \\mathbf{\\Lambda}_c^{\\mathbf{\\Lambda}_c^}^{\\mathrm{T}} \\\\ & = \\mathbf{\\Lambda}_c\\mathbf{T}(\\mathbf{\\Lambda}_c\\mathbf{T})^{\\mathrm{T}} \\\\ & = \\mathbf{\\Lambda}_c\\mathbf{T}\\mathbf{T}^{\\mathrm{T}}\\mathbf{\\Lambda}_c^{\\mathrm{T}} \\\\ \\end{aligned}$$ 进行矩阵乘法,很容易发现 $\\mathbf{T}\\mathbf{T}^{\\mathrm{T}} = \\mathbf{I}$ : $$\\begin{pmatrix} \\cos^2\\alpha + \\sin^2\\alpha = 1 & -\\cos\\alpha\\sin\\alpha + \\sin\\alpha\\cos\\alpha = 0 \\\\ \\sin\\alpha\\cos\\alpha - \\cos\\alpha\\sin\\alpha = 0 & \\cos^2\\alpha + \\sin^2\\alpha = 1 \\\\ \\end{pmatrix}$$ 更一般地,任何维度的任何正交旋转矩阵都有 $\\mathbf{T}\\mathbf{T}^{\\mathrm{T}} = \\mathbf{I}$。 这是因为矩阵转置实际上将轴按照反方向转了回去,得到了最初的方向。 共同度和被公因子解释的变异以及公因子模型的拟合都不受正交旋转的影响, 这一重要结论在后面我们考虑增强因子解的可解释性时会非常有用。 因子旋转为了找到最合适的旋转,我们必须找到一种方法以目标函数的形式定量化表达通过简化结构我们想要什么。 然后我们搜索所有可能的旋转角度,然后选择矩阵 $\\mathbf{T}$ 使满足旋转后的因子载荷矩阵 $\\mathbf{A} = \\mathbf{\\Lambda}_c\\mathbf{T}$ 对于简化的结构展现出高的目标函数的值。 有许多不同的方法来量化最简结构,我们只讨论两个: Kaiser 的最大方差正交旋转法和四次方最大旋转法。 Kaiser 的最大方差正交旋转法回忆旋转后的载荷矩阵每个元素 $a_{ik}$可以被解释为变量 $i$和公因子 $k$ 间的相关系数。 载荷的平方 $a_{ik}^2$是变量 $i$中由公因子 $k$ 引起的变异的比例。 由于我们所选择的公因子互不相关,所有公因子所能解释的变异大小之和——即所谓的共同度——可以由载荷平方和给出, 也就是 $h_i^2 = \\sum_k a_{ik}^2$。 为了实现最简结构,我们想要找到一个旋转使得平方载荷 $a_{ik}^2$要么接近 1,要么接近 0。 最大方差过程通过关注 $\\mathbf{A}$的列来尝试实现这一目的:它选择旋转矩阵 $\\mathbf{T}$ 来最大化 $a_{ik}^2$的列方差之和。 第 $k$个列方差由下式给出 $$V_k = \\frac{1}{p} \\sum_{i=1}^{p}\\left(a_{ik}^2\\right)^2 - \\frac{1}{p^2}\\left(\\sum_{i=1}^{p}a_{ik}^2\\right)^2$$ 对所有因子 $k$最大化列方差 $V_k$之和等同于最大化下式 $$V = \\sum_{k=1}^{c}\\sum_{i=1}^{p}a_{ik}^4 - \\frac{1}{p}\\sum_{k=1}^{c}\\left(\\sum_{i=1}^{p}a_{ik}^2\\right)^2$$ 注意到当 $a_{ik}^2$的值趋向于 0 或 1 时可以得到最大方差; 根据定义,对于某些因子 $k$,当 $a_{ik}^2$的值趋向于 1,所有其他在矩阵中同一行上的项都趋于 0。 对于最大方差正交旋转法使用归一化平方载荷($\\frac{a_{ik}^2}{h_i^2}$)建立目标函数也是可能的。 如果使用这种方式归一化载荷,$\\frac{a_{ik}^2}{h_i^2}$可以被解释为 变量 $i$由于公因子 $k$引起变异大小的比例。 使用这种归一化确保在选择旋转时所有矩阵由相同的权重 (当一些变量有较低共同度时对于没有归一化的平方载荷则不是这种情况)。 四次方最大旋转法与最大方差正交旋转法关注旋转的因子载荷矩阵 $\\mathbf{A}$ 的列不同,四次方最大旋转法关注行。 四次方最大旋转法的目标函数依赖于正交旋转前后变量的共同度不变这一事实; 因此,表达式 $\\sum_k a_{ik}^2$是常数,与旋转矩阵 $\\mathbf{T}$无关。 对于所有变量,共同度的平方和 $\\sum_i(\\sum_k a_{ik}^2)^2$也是一个常数。 扩展这个表达式得到 $$\\sum_{i=1}^{p}\\sum_{k=1}^{c} a_{ik}^4 + \\sum_{i=1}^{p}\\left(\\sum_{k=1}^{c}\\sum_{j\\neq k}a_{ik}^2a_{ij}^2\\right)$$ 上式的第二项是平方载荷的交叉积。当矩阵具有简单结构时,该积应当尽可能小 (即当一个变量在因子 $k$上有高载荷,对其他所有因子 $j$载荷应当接近 0)。 因为上式对于所有旋转矩阵 $\\mathbf{T}$ 是常数,一种保证交叉积项最小的方法是最大化表达式的第一项。 因此,四次方最大旋转法选择一个正交旋转矩阵 $\\mathbf{T}$使 $$Q = \\sum_{i=1}^{p}\\sum_{k=1}^{c} a_{ik}^4$$ 最大。与最大方差正交旋转法相同,在进行旋转之前,可以对平方载荷通过除以变量的共同度进行归一化。 我们在下面的章节中讨论倾斜旋转(旋转不能保持因子的相互正交性)的问题。 因子得分通常,因子分析本身并不是目的,而是进一步分析数据的中间步骤。 对于后续分析,我们需要获得在缩减的因子空间中每个原始观测的位置。 这个值被称为。 从公因子模型中得到因子得分并不像从主成分分析中得到成分得分一样容易。 回想一下,主成分得分是原始变量的线性组合,可以使用来自相关矩阵的特征向量的系数直接计算。 在公因子分析中,由于特定因素引入的不确定性,得分无法准确计算。 换句话说,在公因子模型中我们不能将 $\\xi$在不知道 $\\delta$的情况下写成 $X$的函数。 因此,有必要估计我们将在计算因子得分时使用的因子得分系数。 我们用下述线性表达式近似 $\\mathbf{\\Xi}$ $$\\mathbf{\\Xi} = \\mathbf{X}_s\\mathbf{B}$$ 其中 $\\mathbf{B}$时因子得分系数矩阵。由于 $\\mathbf{\\Xi}$ 的值不能直接被观测到,我们不能使用最小二乘回归计算 $\\mathbf{B}$。 但是,如果我们对上式两边同时乘以 $\\frac{1}{(n-1)}\\mathbf{X}_s^{\\mathrm{T}}$我们可以得到 $$\\frac{1}{n-1}\\mathbf{X}_s^{\\mathrm{T}}\\mathbf{\\Xi} = \\frac{1}{n-1}\\mathbf{X}_s^{\\mathrm{T}}\\mathbf{X}_s\\mathbf{B}$$ 或 $$\\mathbf{\\Lambda}_c = \\mathbf{R}\\mathbf{B}$$ 在保证等式不变的情况下,我们将两边同时乘以 $\\mathbf{R}^{-1}$,对于因子得分系数有 $$\\mathbf{B} = \\mathbf{R}^{-1}\\mathbf{\\Lambda}_c$$ 将这个式子中的 $\\mathbf{B}$ 代入式中得到用于估计因子得分的下式 $$\\mathbf{\\Xi} = \\mathbf{X}_s \\mathbf{R}^{-1} \\mathbf{\\Lambda}_c$$ 注意到由于载荷矩阵 $\\mathbf{\\Lambda}_c$ 满足旋转不确定性,式得到的因子得分 并不唯一,并且具有相同的旋转不确定性。但是乘积 $\\hat{\\mathbf{X}}_c = \\mathbf{\\Xi}\\mathbf{\\Lambda}_c^{\\mathrm{T}}$是不变的, 其中 $\\hat{\\mathbf{X}}_c$由 $c$ 个潜在因子拟合的 $\\mathbf{X}$的一部分。 [^1]: 译者注:原文中没有明确表示”不同”的 $i$和 $j$,但此处确实应不相同。 若相同,则 $\\mathrm{corr}(\\delta_i,\\delta_j) = 1$ 而不是 $0$。 [^2]: 原文:Provides limited relief.","categories":[{"name":"理论","slug":"理论","permalink":"http://hpdell.github.io/categories/理论/"}],"tags":[{"name":"统计学","slug":"统计学","permalink":"http://hpdell.github.io/tags/统计学/"}]},{"title":"让 Windows 的 R 用上 CUDA","slug":"windows-r-cuda","date":"2019-08-12T21:08:25.000Z","updated":"2022-04-14T16:50:55.581Z","comments":true,"path":"编程/windows-r-cuda/","link":"","permalink":"http://hpdell.github.io/编程/windows-r-cuda/","excerpt":"R 是一个统计学经常用到的软件,提供了非常多的统计学函数。 但是它是一个单线程解释语言,面对大数据量的时候,往往性能跟不上,可以利用 Rcpp 编写 C++ 包提供给 R 使用,可以大大提高性能。 而对于大规模数据的处理,使用 CUDA 则是一个非常好的解决方案。 在 Linux 和 macOS 下, CUDA 程序和 C++ 程序都使用 gcc 编译器, 但是在 Windows 下,Rcpp 的包必须用 MinGW 编译器, CUDA 的包必须用 MSVC 编译器,需要一定的技巧才能让 R 用上 CUDA。 本文介绍 MSVC 包和 MinGW 包的混合编译,不仅适用于 R 语言,也不仅适用于 CUDA 程序, 也适用于其他需要通过 MinGW 的程序调用 MSVC 程序的情况。","text":"R 是一个统计学经常用到的软件,提供了非常多的统计学函数。 但是它是一个单线程解释语言,面对大数据量的时候,往往性能跟不上,可以利用 Rcpp 编写 C++ 包提供给 R 使用,可以大大提高性能。 而对于大规模数据的处理,使用 CUDA 则是一个非常好的解决方案。 在 Linux 和 macOS 下, CUDA 程序和 C++ 程序都使用 gcc 编译器, 但是在 Windows 下,Rcpp 的包必须用 MinGW 编译器, CUDA 的包必须用 MSVC 编译器,需要一定的技巧才能让 R 用上 CUDA。 本文介绍 MSVC 包和 MinGW 包的混合编译,不仅适用于 R 语言,也不仅适用于 CUDA 程序, 也适用于其他需要通过 MinGW 的程序调用 MSVC 程序的情况。 MinGW 调用 MSVC 库函数的条件MinGW 其实一直都是可以直接调用 MSVC 库函数的,只是这样的函数需要满足几个条件: MinGW 可以调用 MSVC 编译的动态库,但是不能调用 MSVC 编译的静态库, 因为 MinGW 和 MSVC 中会引用同样的符号,但 MSVC 有的符号在 MinGW 中没有,调用 MSVC 静态库时需要加载 MSVC 的符号,导致冲突。 MSVC 编译的 DLL 必须导出 C 接口,否则 MinGW 中找不到符号,这是因为 C++ 会给函数名做修改以支持函数重载,但 MSVC 和 MinGW 对函数修改的方式不一样。 也有人说时 __cdecl__ 和 __stdcall__ 的问题,但我试了一下不太行,还是找不到符号。 既然时 C 接口,那么参数和返回值不能是 class ,只能用指向 class 类型的指针类型。 不能将 MinGW 中创建的指针传递到 MSVC 中进行操作,否则在 MSVC 中就相当于野指针。反之也不可以。 因为 MinGW 和 MSVC 编译的库,内存地址是两套,指针不互通。 在 R 中,一定会大量用到矩阵,一有矩阵那必然涉及到指针,而且一定是在 MinGW 函数中创建的指针。那么该怎么将矩阵传到 MSVC 的函数中呢? 这其实非常类似于 CUDA 编程中的内存问题,我们只需要在两边分别开辟内存,然后将内存中的数据复制一下,相当于写一个 cudaMalloc 和 cudaMemcpy 。 这样相当于在全局创建了很多的变量,如果使用一些方法将这些全局变量统一管理会更好。 可以将这些全局变量保存在一个结构体中,工厂函数返回一个指向这个结构体的指针,并为这个结构体成员创建初始值。 也可以利用 C++ 类的封装我们要在 R 中调用的函数,将这个类继承自一个抽象类(所有函数都是纯虚函数),将接口类导出, 同时导出一个 C 的工厂函数,返回指向这个接口类的指针, 利用 MSVC 和 MinGW 虚表结构一致的特点,就可以在 MinGW 的 C++ 代码中使用这个接口类中的函数了(类没有虚析构函数)。 将矩阵数据作为类的成员变量,利用 C++ 成员函数进行创建、赋值、销毁。 非类的写法首先演示一种纯 C 非类的写法。首先需要建立一个 VS 的 CUDA 工程,并设置该项目配置类型为“动态链接库”。 项目目录如下: AddCUDA add.cpp add.h kernel.cu kernel.h AddCUDA.vcxproj 文件 kernel.cu 使用的 CUDA 函数是 VS 中 CUDA 工程自带的模板,做了一些修改, 主要去掉了 goto 语句,并设置了核函数启动配置。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116// kernel.cu#include \"cuda_runtime.h\"#include \"device_launch_parameters.h\"#include <stdio.h>#include \"kernel.h\"__global__ void addKernel(int *c, const int *a, const int *b){ int i = threadIdx.x; c[i] = a[i] + b[i];}// Helper function for using CUDA to add vectors in parallel.bool addWithCuda(int *c, const int *a, const int *b, unsigned int size){ int *dev_a = 0; int *dev_b = 0; int *dev_c = 0; cudaError_t cudaStatus; // Choose which GPU to run on, change this on a multi-GPU system. cudaStatus = cudaSetDevice(0); if (cudaStatus != cudaSuccess) { fprintf(stderr, \"cudaSetDevice failed! Do you have a CUDA-capable GPU installed?\"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); return false; } // Allocate GPU buffers for three vectors (two input, one output) . cudaStatus = cudaMalloc((void**)&dev_c, size * sizeof(int)); if (cudaStatus != cudaSuccess) { fprintf(stderr, \"cudaMalloc failed!\"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); return false; } cudaStatus = cudaMalloc((void**)&dev_a, size * sizeof(int)); if (cudaStatus != cudaSuccess) { fprintf(stderr, \"cudaMalloc failed!\"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); return false; } cudaStatus = cudaMalloc((void**)&dev_b, size * sizeof(int)); if (cudaStatus != cudaSuccess) { fprintf(stderr, \"cudaMalloc failed!\"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); return false; } // Copy input vectors from host memory to GPU buffers. cudaStatus = cudaMemcpy(dev_a, a, size * sizeof(int), cudaMemcpyHostToDevice); if (cudaStatus != cudaSuccess) { fprintf(stderr, \"cudaMemcpy failed!\"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); return false; } cudaStatus = cudaMemcpy(dev_b, b, size * sizeof(int), cudaMemcpyHostToDevice); if (cudaStatus != cudaSuccess) { fprintf(stderr, \"cudaMemcpy failed!\"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); return false; } // Launch a kernel on the GPU with one thread for each element. dim3 blockSize(256), gridSize((size + blockSize.x - 1) / blockSize.x); addKernel<<<gridSize, blockSize >>>(dev_c, dev_a, dev_b); // Check for any errors launching the kernel cudaStatus = cudaGetLastError(); if (cudaStatus != cudaSuccess) { fprintf(stderr, \"addKernel launch failed: %s\\n\", cudaGetErrorString(cudaStatus)); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); return false; } // cudaDeviceSynchronize waits for the kernel to finish, and returns // any errors encountered during the launch. cudaStatus = cudaDeviceSynchronize(); if (cudaStatus != cudaSuccess) { fprintf(stderr, \"cudaDeviceSynchronize returned error code %d after launching addKernel!\\n\", cudaStatus); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); return false; } // Copy output vector from GPU buffer to host memory. cudaStatus = cudaMemcpy(c, dev_c, size * sizeof(int), cudaMemcpyDeviceToHost); if (cudaStatus != cudaSuccess) { fprintf(stderr, \"cudaMemcpy failed!\"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); return false; } return true;} 将 addWithCuda 函数的声明移动到 kernel.h 文件中。 1234// kernel.h#pragma once#include <cuda_runtime.h>bool addWithCuda(int *c, const int *a, const int *b, unsigned int size); 然后需要编写 MSVC 的接口函数了。在 add.h 文件中做如下声明: 1234567891011121314// add.h#pragma once#ifdef DLL_EXPORT#define ADDCUDA_API extern \"C\" __declspec(dllexport)#else#define ADDCUDA_API extern \"C\" __declspec(dllimport)#endif // DLL_EXPORTADDCUDA_API int* createVector(int n);ADDCUDA_API void setVector(int* ptr, int i, int value);ADDCUDA_API int getVector(int* ptr, int i);ADDCUDA_API void deleteVector(int* ptr);ADDCUDA_API bool addVector(int* a, int* b, int n, int *c); 当然, __declspec(dllimport) 可要可不要,要也可以,这样这个 Dll 也可以给 Windows 程序使用。 在 add.cpp 中进行定义: 123456789101112131415161718192021222324252627282930// add.cpp#include \"add.h\"#include \"kernel.h\"int* createVector(int n){ return new int[n];}void deleteVector(int* ptr){ delete[] ptr;}void setVector(int* ptr, int i, int value){ ptr[i] = value;}int getVector(int* ptr, int i){ return ptr[i];}bool addVector(int* a, int* b, int n, int *c){ bool cudaStatus = addWithCuda(c, a, b, n); return cudaStatus;} 虽然这几个函数很短小,但是也不能写在头文件里。否则 MinGW 中会报符号二义性错误。 然后在 MinGW 的主函数中进行调用 123456789101112131415161718192021222324252627282930313233343536// mingw.cpp#include \"add.h\"#include <stdio.h>int main(int argc, char const *argv[]){ int n = 100000; int *a = createVector(n); int *b = createVector(n); int *c = createVector(n); for (size_t i = 0; i < n; i++) { setVector(a, i, 10); setVector(b, i, 100); setVector(c, i, 0); } addVector(a, b, n, c); int *result = new int[n]; for (size_t i = 0; i < n; i++) { result[i] = getVector(c, i); } printf(\"result:\\n\"); for (size_t i = 0; i < 10; i++) { printf(\"%5d\", result[i]); } printf(\"\\n\"); deleteVector(a); deleteVector(b); deleteVector(c); return 0;} MinGW 在链接时,需要手动指定 MSVC 生成的 lib 文件,而且要放到 -o 参数的后面,方法如下: 1g++ -I\"./AddCUDA\" -L\"./x64/Release\" mingw.o -o cudaMinGWC -lAddCUDA 这样就生成了 cudaMinGWC.exe 文件。运行一下可以得到结果: 12result: 110 110 110 110 110 110 110 110 110 110 抽象类的写法抽象类的写法主要用到了多态的特性。首先需要创建一个抽象基类 IAdd 和派生类 CAdd 。 1234567891011121314151617181920212223242526272829303132333435// IAdd.h#pragma once#ifdef DLL_EXPORT#define ADDCUDA_API __declspec(dllexport)#else#define ADDCUDA_API __declspec(dllimport)#endif // DLL_EXPORTclass ADDCUDA_API IAdd{public: virtual void SetA(int i, int value) = 0; virtual void SetB(int i, int value) = 0; virtual int GetC(int i) = 0; virtual bool Add() = 0;};extern \"C\" ADDCUDA_API IAdd* Add_new(int n);extern \"C\" ADDCUDA_API void Add_del(IAdd* ptr);// IAdd.cpp#include \"IAdd.h\"#include \"CAdd.h\"IAdd* Add_new(int n){ return new CAdd(n);}void Add_del(IAdd* ptr){ delete ptr;} 在抽象类中完全不实现类的任何接口,都标记为纯虚函数。 同时,在抽象类的外面,定义一套工厂函数,用于创建和销毁这个抽象类派生类的对象。 派生类的定义如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566// CAdd.h#pragma once#include \"IAdd.h\"class CAdd : public IAdd{private: int n; int* a; int* b; int* c;public: CAdd(int n); ~CAdd(); virtual void SetA(int i, int value); virtual void SetB(int i, int value); virtual int GetC(int i); virtual bool Add();};// CAdd.cpp#include \"CAdd.h\"#include \"kernel.h\"#include <memory.h>CAdd::CAdd(int n){ this->n = n; a = new int[n]; b = new int[n]; c = new int[n]; memset(a, 0, sizeof(int) * n); memset(b, 0, sizeof(int) * n); memset(c, 0, sizeof(int) * n);}CAdd::~CAdd(){ delete[] a; delete[] b; delete[] c;}void CAdd::SetA(int i, int value){ if (i < n) a[i] = value;}void CAdd::SetB(int i, int value){ if (i < n) b[i] = value;}int CAdd::GetC(int i){ return (i < n) ? c[i] : 0;}bool CAdd::Add(){ return addWithCuda(c, a, b, n);} 在 MinGW 中调用如下: 1234567891011121314151617181920212223#include <stdio.h>#include \"IAdd.h\"int main(int argc, char const *argv[]){ int n = 100000; IAdd* ptr = Add_new(n); for (size_t i = 0; i < n; i++) { ptr->SetA(i, 10); ptr->SetB(i, 100); } ptr->Add(); printf(\"result:\\n\"); for (size_t i = 0; i < 10; i++) { printf(\"%5d\", ptr->GetC(i)); } printf(\"\\n\"); Add_del(ptr); return 0;} 程序运行结果: 12result: 110 110 110 110 110 110 110 110 110 110 可见已经可以运行了。 总结总体而言,这种方式调用方式的开销还是比较大的。 不仅在内存中复制了一份数据,在传递数据的过程中是一个一个传递的,比直接内存拷贝开销大很多。 另外如果采用抽象类的方式,还有虚函数调用的开销。 因此,如果不是必须在 Windows 上用 MinGW 调用 CUDA 程序,尽量还是使用 MSVC 编译器。 对于 Rcpp 而言,恰恰是必须在 Windows 使用 MinGW ,此使想调用 CUDA 程序,则需要通过这种方式。 本文所涉及代码已发布在 GitHub 上。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"R","slug":"R","permalink":"http://hpdell.github.io/tags/R/"},{"name":"CUDA","slug":"CUDA","permalink":"http://hpdell.github.io/tags/CUDA/"}]},{"title":"HTML5 播放 RTSP 视频","slug":"html5-rtsp","date":"2019-03-04T15:01:25.000Z","updated":"2022-04-14T16:50:55.449Z","comments":true,"path":"编程/html5-rtsp/","link":"","permalink":"http://hpdell.github.io/编程/html5-rtsp/","excerpt":"","text":"目前大多数网络摄像头都是通过 RTSP 协议传输视频流的,但是 HTML 并不标准支持 RTSP 流。除了 Firefox 浏览器可以直接播放 RTSP 流之外,几乎没有其他浏览器可以直接播放 RTSP 流。Electron 应用是基于 Chromium 内核的,因此也不能直接播放 RTSP 流。 在借助一定工具的情况下,可以实现在 Web 页面上播放 RTSP 流。本文介绍的方法可以应用于传统 Web 应用和 Electron 应用中,唯一的区别是将 Electron 应用的主进程当作传统 Web 应用的服务器。 目前已有 RTSP 播放方案的对比既然是做直播,就需要延迟较低。当摄像头掉线时,也应当有一定的事件提示。处于这两点,对目前已有的已经实现、无需购买许可证的 RTSP 播放方案进行对比(处于原理阶段的暂时不分析)。 方案 协议 视频格式 延迟 离线事件汇报 最小端口占用 依赖 1 HLS ogg 网络延迟较高,可达10秒以上 难 n VLC + video.js 2 RTMP flv 网络延迟较低,5秒左右 难 n ffmpeg + nginx + flash + video.js 3 WebSocket mpegts 网络延迟很低,渲染速度慢 易 1 ffmpeg + express + jsmpeg 4 HTTP-FLV flv 网络延迟很低,渲染速度快 易 1 ffmpeg + express + flv.js 我对这四种方式都进行了实现,整体效果最好的还是第4种方案,占用端口少,延迟低,渲染速度快,而且离线事件易于处理。 基于 flv.js 的 RTSP 播放方案flv.js 是 Bilibili 开源的一款 HTML5 浏览器。依赖于 Media Source Extension 进行视频播放,视频通过 HTTP-FLV 或 WebSocket-FLV 协议传输,视频格式需要为 FLV 格式。 服务器端(主进程)服务器端采用 express + express-ws 框架进行编写,当有 HTTP 请求发送到指定的地址时,启动 ffmpeg 串流程序,直接将 RTSP 流封装成 FLV 格式的视频流,推送到指定的 WebSocket 响应流中。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import * as express from \"express\";import * as expressWebSocket from \"express-ws\";import ffmpeg from \"fluent-ffmpeg\";import webSocketStream from \"websocket-stream/stream\";import WebSocket from \"websocket-stream\";import * as http from \"http\";function localServer() { let app = express(); app.use(express.static(__dirname)); expressWebSocket(app, null, { perMessageDeflate: true }); app.ws(\"/rtsp/:id/\", rtspRequestHandle) app.listen(8888); console.log(\"express listened\")}function rtspRequestHandle(ws, req) { console.log(\"rtsp request handle\"); const stream = webSocketStream(ws, { binary: true, browserBufferTimeout: 1000000 }, { browserBufferTimeout: 1000000 }); let url = req.query.url; console.log(\"rtsp url:\", url); console.log(\"rtsp params:\", req.params); try { ffmpeg(url) .addInputOption(\"-rtsp_transport\", \"tcp\", \"-buffer_size\", \"102400\") // 这里可以添加一些 RTSP 优化的参数 .on(\"start\", function () { console.log(url, \"Stream started.\"); }) .on(\"codecData\", function () { console.log(url, \"Stream codecData.\") // 摄像机在线处理 }) .on(\"error\", function (err) { console.log(url, \"An error occured: \", err.message); }) .on(\"end\", function () { console.log(url, \"Stream end!\"); // 摄像机断线的处理 }) .outputFormat(\"flv\").videoCodec(\"copy\").noAudio().pipe(stream); } catch (error) { console.log(error); }} 当然这个实现还比较粗糙。当有多个相同地址的请求时,应当增加 ffmpeg 的输出,而不是启动一个新的 ffmpeg 进程串流。 浏览器端(渲染进程)示例使用 Vue 框架进行页面设计。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152<template> <div> <video class="demo-video" ref="player"></video> </div></template><script>import flvjs from "flv.js";export default { props: { rtsp: String, id: String }, /** * @returns {{player: flvjs.Player}} */ data () { return { player: null } }, mounted () { if (flvjs.isSupported()) { let video = this.$refs.player; if (video) { this.player = flvjs.createPlayer({ type: "flv", isLive: true, url: `ws://localhost:8888/rtsp/${this.id}/?url=${this.rtsp}` }); this.player.attachMediaElement(video); try { this.player.load(); this.player.play(); } catch (error) { console.log(error); }; } } }, beforeDestroy () { this.player.destory(); }}</script><style> .demo-video { max-width: 480px; max-height: 360px; }</style> 效果展示用 Electron 页面展示了 7 个 Hikvison NVR 的摄像头,可以实现低延迟,低 CPU 占用,无花屏现象。由于涉及隐私,这里就不放截图了。 同样的方法我播放了 9 个本地 1080p 的视频《白鹿原》,可以看一下这个效果。 播放效果非常好,完全没有卡顿和花屏,CPU 占用率也不高。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"网页开发","slug":"网页开发","permalink":"http://hpdell.github.io/tags/网页开发/"},{"name":"Node.js","slug":"Node-js","permalink":"http://hpdell.github.io/tags/Node-js/"}]},{"title":"Microsoft Machine Learning Server 使用调研","slug":"microsoft-machine-learning-server","date":"2019-03-04T15:01:25.000Z","updated":"2022-04-14T16:50:55.521Z","comments":true,"path":"编程/microsoft-machine-learning-server/","link":"","permalink":"http://hpdell.github.io/编程/microsoft-machine-learning-server/","excerpt":"","text":"Machine Learning Server 主要功能Micorosft Machine Learning Server 前身就是 Microsoft R Server ,加入了 Python 支持后更名。主要用于数据分析模型的商业化使用,并提供分布式、并行等计算功能。 支持以下平台: Windows Hadoop Linux Machine Learning Server 主要提供以下功能: 功能 R Python 在 Machine Learning Server 中运行 R 代码 o 使用 RevoScalePy 创建 Python 代码 o 使用 MicrosoftML 进行二分分类 o 将模型作为 Web 服务发布 o o Machine Learning Server 的特性: 无论您的数据存在于何处,都可以获得高性能的机器学习 微软和开源的最佳人工智能创新 简单,安全,高规模的操作和管理 深入的生态系统参与,以最佳的总体拥有成本实现客户成功 Machine Learning Server 的使用是与 Microsoft R Client 结合进行的。Microsoft R Client 相当于一个 R 的发行版,内置了一些专用的函数,可以进行远程运行代码以及发布 Web 服务。 RevoScaleR 库提供了一系列 rx 开头的函数,这些函数可以与 Machine Learning Server 结合运行。 数据处理rxImport 从文本文件、 SAS 、 SPSS 、 SQL Server 、 Teradata 或 ODBC 连接导入数据。原生的数据类型是 XDF 格式。 123inDataFile <- file.path(rxGetOption(\"sampleDataDir\"), \"mortDefaultSmall2000.csv\")mortOutput <- NULLmortData <- rxImport(inData = inDataFile, outFile = mortOutput) 关于 Machine Learning Server中的数据存储 在 Machine Learning Server 中,您可以将内存数据用作数据框,或将其作为 XDF 文件保存到磁盘。如上所述, rxImport 可以使用或不使用 .xdf 文件创建数据源对象。如果省略 outFile 参数或将其设置为 NULL,则返回对象是包含数据的数据框。 数据框是 R 中的基本数据结构,在机器学习服务器中完全支持。它是表格,由行和列组成,其中列包含变量,第一行(称为标题)存储列名。后续行为与单个观察相关联的每个变量提供数据值。数据框是在加载某些数据时创建的临时数据结构。它仅在会话期间存在。 .xdf 文件是 Machine Learning Server 本机的二进制文件格式,用于在磁盘上保存数据。 .xdf 文件的组织是基于列的,每个变量一列,这对于统计和预测分析中使用的数据的可变方向是最佳的。使用 .xdf ,您可以加载数据的子集以进行目标分析。此外, .xdf 文件包括可立即使用的预先计算的元数据,无需额外处理。 不需要创建 .xdf ,但是当数据集很大或很复杂时, .xdf 文件可以通过压缩数据和将数据分配到可以独立读取和刷新的块来提供帮助。此外,在像 Hadoop 的 HDFS 这样的分布式文件系统上, XDF 文件可以将数据存储在多个物理文件中以容纳非常大的数据集。 rxGetInfo 一次快速获取有关数据集及其变量的信息,包括有关变量类型和范围的更多信息。 123456789101112131415161718rxGetInfo(mortData, getVarInfo = TRUE, numRows=3)# 输出# Data frame: mortData# Data frame: mortData# Number of observations: 10000# Number of variables: 6# Variable information:# Var 1: creditScore, Type: integer, Low/High: (486, 895)# Var 2: houseAge, Type: integer, Low/High: (0, 40)# Var 3: yearsEmploy, Type: integer, Low/High: (0, 14)# Var 4: ccDebt, Type: integer, Low/High: (0, 12275)# Var 5: year, Type: integer, Low/High: (2000, 2000)# Var 6: default, Type: integer, Low/High: (0, 1)# Data (3 rows starting with row 1):# creditScore houseAge yearsEmploy ccDebt year default# 1 691 16 9 6725 2000 0# 2 691 4 4 5077 2000 0# 3 743 18 3 3080 2000 0 rxDataStep 为大多数数据操作任务提供了一个框架。 它允许行选择(rowSelection参数),变量选择(varsToKeep或varsToDrop参数),以及从现有变量创建新变量(变换参数)。 123456789101112131415outFile2 <- NULLmortDataNew <- rxDataStep( # Specify the input data set inData = mortData, # Put in a placeholder for an output file outFile = outFile2, # Specify any variables to keep or drop varsToDrop = c(\"year\"), # Specify rows to select rowSelection = creditScore < 850, # Specify a list of new variables to create transforms = list( catDebt = cut(ccDebt, breaks = c(0, 6500, 13000), labels = c(\"Low Debt\", \"High Debt\")), lowScore = creditScore < 625)) 数据分析 rxSummary 计算变量的表属性统计信息 123456789101112rxSummary(~ ArrDelay, data = airXdfData)# 输出# Call:# rxSummary(formula = ~ArrDelay, data = airXdfData)## Summary Statistics Results for: ~ArrDelay# Data: airXdfData (RxXdfData Data Source)# File name: airExample.xdf# Number of valid observations: 6e+05## Name Mean StdDev Min Max ValidObs MissingObs# ArrDelay 11.31794 40.68854 -86 1490 582628 17372 123456789101112131415161718192021222324rxSummary(formula = ~ArrDelay + CRSDepTime + DayOfWeek, data = airDS)# 输出# Summary Statistics Results for: ~ArrDelay + CRSDepTime + DayOfWeek# Data: airXdfData (RxXdfData Data Source)# File name: C:\\Users\\TEMP\\airExample.xdf# Number of valid observations: 6e+05# # Name Mean StdDev Min Max ValidObs MissingObs# ArrDelay 11.31794 40.688536 -86.000000 1490.00000 582628 17372 # CRSDepTime 13.48227 4.697566 0.016667 23.98333 600000 0 # # Category Counts for DayOfWeek# Number of categories: 7# Number of valid observations: 6e+05# Number of missing observations: 0# # DayOfWeek Counts# Monday 97975# Tuesday 77725# Wednesday 78875# Thursday 81304# Friday 82987# Saturday 86159# Sunday 94975 123456789101112131415161718192021rxSummary(~ArrDelay:DayOfWeek, data=airXdfData)# Call:# rxSummary(formula = ~ArrDelay:DayOfWeek, data=airXdfData)# Summary Statistics Results for: ~ArrDelay:DayOfWeek# File name: C:\\Users\\TEMP\\airExample.xdf# Number of valid observations: 6e+05# Name Mean StdDev Min Max ValidObs MissingObs# ArrDelay:DayOfWeek 11.31794 40.68854 -86 1490 582628 17372 # Statistics by category (7 categories):# Category DayOfWeek Means StdDev Min Max ValidObs# ArrDelay for DayOfWeek=Monday Monday 12.025604 40.02463 -76 1017 95298 # ArrDelay for DayOfWeek=Tuesday Tuesday 11.293808 43.66269 -70 1143 74011 # ArrDelay for DayOfWeek=Wednesday Wednesday 10.156539 39.58803 -81 1166 76786 # ArrDelay for DayOfWeek=Thursday Thursday 8.658007 36.74724 -58 1053 79145 # ArrDelay for DayOfWeek=Friday Friday 14.804335 41.79260 -78 1490 80142 # ArrDelay for DayOfWeek=Saturday Saturday 11.875326 45.24540 -73 1370 83851 # ArrDelay for DayOfWeek=Sunday Sunday 10.331806 37.33348 -86 1202 93395 rxLinMod 线性回归 123456789101112131415161718192021222324252627282930arrDelayLm2 <- rxLinMod(ArrDelay ~ DayOfWeek, data = airXdfData, cube = TRUE)summary(arrDelayLm2)# Call:# rxLinMod(formula = ArrDelay ~ DayOfWeek, data = airXdfData, cube = TRUE)# Cube Linear Regression Results for: ArrDelay ~ DayOfWeek# File name: C:\\Users\\Temp\\airExample.xdf# Dependent variable(s): ArrDelay# Total independent variables: 7# Number of valid observations: 582628# Number of missing observations: 17372# Coefficients:# Estimate Std. Error t value Pr(>|t|) | Counts# DayOfWeek=Monday 12.0256 0.1317 91.32 2.22e-16 *** | 95298# DayOfWeek=Tuesday 11.2938 0.1494 75.58 2.22e-16 *** | 74011# DayOfWeek=Wednesday 10.1565 0.1467 69.23 2.22e-16 *** | 76786# DayOfWeek=Thursday 8.6580 0.1445 59.92 2.22e-16 *** | 79145# DayOfWeek=Friday 14.8043 0.1436 103.10 2.22e-16 *** | 80142# DayOfWeek=Saturday 11.8753 0.1404 84.59 2.22e-16 *** | 83851# DayOfWeek=Sunday 10.3318 0.1330 77.67 2.22e-16 *** | 93395# ---# Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1# Residual standard error: 40.65 on 582621 degrees of freedom# Multiple R-squared: 0.001869 (as if intercept included)# Adjusted R-squared: 0.001858# F-statistic: 181.8 on 6 and 582621 DF, p-value: < 2.2e-16# Condition number: 1 rxCrossTabs 数据交叉表 12345678910111213141516171819202122myTab <- rxCrossTabs(ArrDelay~DayOfWeek, data = airLateDS)summary(myTab, output = \"means\")# Call:# rxCrossTabs(formula = ArrDelay ~ DayOfWeek, data = airLateDS)# Cross Tabulation Results for: ArrDelay ~ DayOfWeek# File name: C:\\YourWorkingDir\\ADS1.xdf# Dependent variable(s): ArrDelay# Number of valid observations: 148526# Number of missing observations: 0# Statistic: means# ArrDelay (means):# means means %# Monday 56.94491 13.96327# Tuesday 64.28248 15.76249# Wednesday 60.12724 14.74360# Thursday 55.07093 13.50376# Friday 56.11783 13.76047# Saturday 61.92247 15.18380# Sunday 53.35339 13.08261# Col Mean 57.96692 rxLogit Logistic 回归 1234567891011121314151617181920212223logitObj <- rxLogit(Late~DepHour + Night, data = airExtraDS)summary(logitObj)# Call:# rxLogit(formula = Late ~ DepHour + Night, data = airExtraDS)# Logistic Regression Results for: Late ~ DepHour + Night# File name: C:\\Users\\Temp\\ADS2.xdf# Dependent variable(s): Late# Total independent variables: 3# Number of valid observations: 582628# Number of missing observations: 17372# -2*LogLikelihood: 649607.8613 (Residual deviance on 582625 degrees of freedom)# Coefficients:# Estimate Std. Error z value Pr(>|z|) # (Intercept) -2.0990076 0.0104460 -200.94 2.22e-16 ***# DepHour 0.0790215 0.0007671 103.01 2.22e-16 ***# Night -0.3027030 0.0109914 -27.54 2.22e-16 ***# ---# Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1# Condition number of final variance-covariance matrix: 3.0178# Number of iterations: 4 rxPredict 预测 123456789101112131415161718192021222324predictDS <- rxPredict(modelObject = logitObj, data = airExtraDS, outData = airExtraDS)rxGetInfo(predictDS, getVarInfo=TRUE, numRows=5)# File name: C:\\Users\\Temp\\ADS2.xdf# Number of observations: 6e+05# Number of variables: 7# Number of blocks: 2# Compression type: zlib# Variable information:# Var 1: ArrDelay, Type: integer, Low/High: (-86, 1490)# Var 2: CRSDepTime, Type: numeric, Storage: float32, Low/High: (0.0167, 23.9833)# Var 3: DayOfWeek# 7 factor levels: Monday Tuesday Wednesday Thursday Friday Saturday Sunday# Var 4: Late, Type: logical, Low/High: (0, 1)# Var 5: DepHour, Type: integer, Low/High: (0, 23)# Var 6: Night, Type: logical, Low/High: (0, 1)# Var 7: Late_Pred, Type: numeric, Low/High: (0.0830, 0.3580)# Data (5 rows starting with row 1):# ArrDelay CRSDepTime DayOfWeek Late DepHour Night Late_Pred# 1 6 9.666666 Monday FALSE 9 FALSE 0.1997569# 2 -8 19.916666 Monday FALSE 19 FALSE 0.3548931# 3 -2 13.750000 Monday FALSE 13 FALSE 0.2550745# 4 1 11.750000 Monday FALSE 11 FALSE 0.2262214# 5 -2 6.416667 Monday FALSE 6 FALSE 0.1645331 数据可视化R 中常用的可视化函数都有对应的 rx 版本。 rxHistogram 绘制直方图 1rxHistogram(~ArrDelay|DayOfWeek, data = airXdfData) rxLinePlot 绘制折线图 12rxLinePlot(ArrDelay~DayOfWeek, data = countsDF, main = \"Average Arrival Delay by Day of Week\") 12rxLinePlot(ArrDelay~CRSDepTime|DayOfWeek, data = arrDelayDT, title = \"Average Arrival Delay by Day of Week by Departure Hour\") 在 Machine Learning Server 中运行 R 代码计算上下文在 Machine Learning Server 中,计算上下文是指处理给定工作负载的计算引擎的物理位置。 默认为本地。 但是,如果您有多台计算机,则可以从本地切换到远程,将以数据为中心的RevoScaleR(R),revoscalepy(Python),MicrosoftML(R)和microsoftml(Python)函数的执行推送到另一个系统上的计算引擎。 例如,在R Client中本地运行的脚本可以将执行转移到Spark集群中的远程机器学习服务器以在那里处理数据。 上下文 说明 local 所有平台上的所有产品(包括R Client)均支持默认值。 脚本使用本地计算机资源在本地解释器上执行。 remote 专门针对选定数据平台上的机器学习服务器:Spark over Hadoop分布式文件系统(HDFS)和SQL Server。 客户端或以客户端身份运行的服务器可以启动远程计算上下文,但目标远程计算机本身必须是Machine Learning Server安装。 RevoScaleR 的上下文包括: 上下文 别名 用法 RxLocalSeq local 所有服务器和客户端配置都支持本地计算上下文。 RxSpark spark 远程计算上下文。 目标是Hadoop上的Spark集群。 RxInSqlServer sqlserver 远程计算上下文。 目标服务器是单个数据库会话(SQL Server 2016 R服务或SQL Server 2017机器学习服务)。 计算是平行的,但不是分布式的。 RxLocalParallel localpar 计算上下文通常用于依靠您提供的指令而不是Hadoop上的内置调度程序来启用受控的分布式计算。 您可以将计算上下文用于手动分布式计算。 RxForeachDoPar dopar 用于手动分布式计算。 上下文支持的数据格式 Data Source RxLocalSeq RxSpark RxInSqlServer RxTextData X X RxXdfData X X RxHiveData X X RxParquetData X X RxOrcData X X RxOdbcData X RxSqlServerData X X RxSasData X RxSpssData X 当数据本身发生变化时,适用于切换上下文。计算应该都是在本地进行的,只是获取数据的上下文发生了改变。 远程执行 R 代码远程执行是从机器学习服务器(或R服务器)或R客户端向另一个机器学习服务器实例上运行的远程会话发出R命令的能力。 您可以使用远程执行来卸载服务器上的繁重处理并测试您的工作。 在开发和测试分析时,它尤其有用。 远程执行支持以下几种方式: 在控制台应用程序中从命令行执行 在 R 脚本中通过调用 mrsdeply 包的函数执行 通过调用 API 的代码执行 远程执行可以实现的功能: 登陆和登出 Machine Learning Server 生成本地和远程环境的差异报告,并协调任何差异 远程执行 R 脚本或代码 远程使用 R 对象或文件工作 创建和管理远程环境的快照以供重用 远程执行的函数: remoteExecute 用于在远程R会话中执行R代码块或R脚本的基本功能。 remoteScript 一个简单的包装函数,用于执行远程R脚本。 diffLocalRemote 在本地和远程之间生成“差异”报告。 创建远程会话使用 mrsdeploy 包的登录函数 remoteLogin() 或 remoteLoginAAD() 在 Machine Learning Server 上进行验证。设置 session = TRUE 创建远程会话,并设置 commandline = TRUE 进入远程控制台。 123456remoteLogin(\"http://localhost:12800\", username = \"admin\", password = \"{{YOUR_PASSWORD}}\", diff = TRUE, session = TRUE, commandline = TRUE) 参数说明: 参数 描述 endpoint Machine Learning Server HTTP / HTTPS端点,包括端口号。 启动管理实用程序时,可以在第一个屏幕上找到此项。 session 如果为 TRUE ,则创建远程会话。 如果省略,则创建远程会话。 diff 如果为 TRUE ,则创建一个“差异”报告,显示本地会话和远程会话之间的差异。 参数仅在会话参数为TRUE时有效。 commandline 如果为 TRUE ,则在R控制台中创建“REMOTE”命令行。参数仅在会话参数为 TRUE 时有效。如果省略,则与 = TRUE 相同。 prompt 用于远程会话的命令提示符。 默认情况下,使用 REMOTE> 。 username 如果为 NULL ,则提示用户输入您的AD或本地计算机学习服务器用户名。 password 如果为 NULL ,则提示用户输入密码。 remoteLoginAAD() 函数用于 Azure 云的登录。 在会话间切换或退出使用三个函数进行远程会话和本地会话切换,以及退出: pause() 从远程会话切换到本地会话 resume() 从本地会话切换到远程会话 remoteLogout() 登出 Machine Learning Server 使用参数 session = TRUE 登录到远程R服务器后,将创建一个远程 R 会话。 您可以直接从命令行在远程 R 会话和本地 R 会话之间切换。 远程命令行允许您直接与另一台计算机上的 R Server 9.x 实例进行交互。 当R控制台中显示 REMOTE> 命令提示符时,输入的任何R命令都在远程R会话上执行。 使用以下函数在本地命令行和远程命令行之间切换:pause() 和 resume()。 要切换回本地 R 会话,请键入“pause()”。 如果已切换到本地R会话,则可以通过键入“ resume()”返回远程 R 会话。 要终止远程R会话,请在REMOTE>提示符下键入“exit”。 此外,要从本地R会话终止远程会话,请键入“remoteLogout()”。 1234567891011#EXAMPLE: SESSION SWITCHING #execute some R commands on the remote sessionREMOTE>x<-rnorm(1000)REMOTE>hist(x)REMOTE>pause() #switches the user to the local R session>resume() REMOTE>exit #logout and terminate the remote R session> 远程执行 R 脚本如果本地计算机上有R脚本,则可以使用 remoteScript()函数远程执行它们。 此函数采用远程执行R脚本的路径。 您还可以选择保存或显示脚本执行期间可能生成的任何图。 该函数返回一个列表,其中包含执行状态(成功/失败),生成的控制台输出以及创建的文件列表。 如果您的R脚本具有R包依赖项,则必须在 Microsoft R Server 上安装这些包。 您的管理员可以在服务器上全局安装它们,也可以使用 install.packages() 函数在远程会话期间自行安装它们。 将 lib 参数留空。 远程上下文的限制: 某些功能在执行时被屏蔽,例如“help”,“browser”,“q”和“quit”。 在远程上下文中,您无法在命令行提示符下显示晕影或获取帮助。 在大多数情况下,“system”命令有效。 但是,写入stdout/stderr的系统命令可能不会显示其输出,也不会等到整个系统命令完成后才显示输出。 install.packages 是我们在远程上下文中显式处理 stdout 和 stderr 的唯一例外。 要在远程脚本执行期间继续在开发环境中工作,可以异步执行 R 脚本。 当您运行具有较长执行时间的脚本时,异步脚本执行非常有用。要异步执行 R 脚本,请将 remoteScript() 的 async 参数设置为 TRUE 。 执行 remoteScript() 时,脚本将在新的远程 R 控制台窗口中异步运行。 所有 R 控制台输出和来自该执行的任何图都返回到同一窗口。 12345678910111213#EXAMPLE: REMOTE SCRIPT EXECUTION #install a package for the life of the sessionREMOTE>install.packages(\"bitops\")#switch to the local R sessionREMOTE>pause()#execute an R script remotely>remoteScript(\"C:/myScript.R\") #execute that script again in another window asynchronously>remoteScript(\"C:/myScript.R\", async=TRUE) 远程使用R对象和文件远程执行R代码后,您可能希望检索某些R对象并将其加载到本地R会话中。 例如,如果您有一个创建线性模型 m <-lm(x~y) 的R脚本,请使用函数 getRemoteObject() 来检索本地R会话中的对象 m 。 相反,如果您希望远程 R 会话可以使用本地 R 对象,则可以使用函数 putLocalObject() 。 如果要同步本地和远程工作空间,可以使用 putLocalWorkspace() 和 getRemoteWorkspace() 函数。 类似的功能可用于需要在本地和远程 R 会话之间移动的文件。以下函数可用于处理文件: putLocalFile() 、 getRemoteFile() 、 listRemoteFiles() 和 deleteRemoteFile() 。 1234567891011121314151617#EXAMPLE: REMOTE R OBJECTS AND FILES #execute a script remotely that generated 2 R objects we are interested in retrieving>remoteExecute(\"C:/myScript.R\")#retrieve the R objects from the remote R session and load them into our local R session>getRemoteObject(c(\"model\",\"out\"))#an R script depends upon an R object named `data` to be available. Move the local#instance of `data` to the remote R session>putLocalObject(\"data\")#execute an R script remotely>remoteScript(\"C:/myScript2.R\")#push a data file to the remote R session>putLocalFile(\"C:/data/survey.csv\")#execute an R script remotely>remoteScript(\"C:/myScript2.R\") 绘图的注意事项远程绘制时,默认绘图大小为400 x 400像素。 如果您需要更高分辨率的输出,则必须告诉远程会话要创建的绘图大小。 在本地会话中,您可以更改宽度和高度,如下所示: 123> png(filename=\"myplot.png\", width=1440, height=900)> ggplot(aes(x=value, group=am, colour=factor(am)), data=mtcarsmelt) + geom_density() + facet_wrap(~variable, scales=\"free\")> dev.off() 在处理REMOTE命令行时,您需要将这三个语句组合在一起: 1REMOTE> png(filename=\"myplot.png\", width=1440, height=900);ggplot(aes(x=value, group=am, colour=factor(am)), data=mtcarsmelt) + geom_density() + facet_wrap(~variable, scales=\"free\");dev.off() 作为替代方法,您可以使用 remoteScript() 函数,如下所示: 123456#Open a new script window in your IDE#Enter the commands on separate linespng(filename=\"myplot.png\", width=1440, height=900)ggplot(aes(x=value, group=am, colour=factor(am)), data=mtcarsmelt) + geom_density() + facet_wrap(~variable, scales=\"free\")dev.off() 将模型作为 Web 服务发布mrsdeploy 库提供了一些函数,使 R 模型可以作为 Web 服务发布。 主要流程 本地编写模型 将模型作为 Web 服务发布 在 R 中使用服务进行测试 获取基于 Swagger 的 JSON 文件 基于 Swagger 文件与 Web 服务集成 Swagger 大部分 Web 应用程序都支持 RESTful API,但不同于 SOAP API——REST API 依赖于 HTTP 方法,缺少与 Web 服务描述语言(Web Services Description Language,WSDL)类似的语言来定义使用者与提供者之间的请求和响应结构。由于没有充分的合同服务,许多 REST API 提供者使用 Microsoft Word 文档或维基页面来记录 API 用法。这些格式使协作和文档版本控制变得很困难,尤其对于有许多 API 或资源的应用程序,或者在 API 采用迭代式开发方式时。这些文档类型在集成到自动化测试应用程序中时变得更难。 开源 Swagger 框架帮助 API 使用者和开发人员纠正了这些问题。该框架为创建 JSON 或 YAML(JSON 的一个人性化的超集)格式的 RESTful API 文档提供了 OpenAPI 规范(以前称为 Swagger 规范)。Swagger 文档可由各种编程语言处理,可在软件开发周期中签入源代码控制系统中,以便进行版本管理。 发布服务服务主要包括模型、代码两部分,模型即指一些模型对象,如 glm 函数的结果;代码是指调用模型的过程,主要对传输参数进行一些处理。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667# For R Server 9.0, load mrsdeploy package on R Server library(mrsdeploy)# --- AAD login ----------------------------------------------------------------# Use `remoteLogin` to authenticate with R Server using # the local admin account. Use session = false so no # remote R session started# REMEMBER: Replace with your login detailsremoteLogin(\"http://localhost:12800\", username = “admin”, password = “{{YOUR_PASSWORD}}”, session = FALSE)# Information can come from a filemodel <- \"model <- glm(formula = am ~ hp + wt, data = mtcars, family = binomial)\"code <- \"newdata <- data.frame(hp = hp, wt = wt)\\n answer <- predict(model, newdata, type = 'response')\"cat(model, file = \"transmission.R\", append = FALSE)cat(code, file = \"transmission-code.R\", append = FALSE)# Generate a unique serviceName for demos # and assign to variable serviceNameserviceName <- paste0(\"mtService\", round(as.numeric(Sys.time()), 0))api <- publishService( serviceName, code = \"transmission-code.R\", model = \"transmission.R\", inputs = list(hp = \"numeric\", wt = \"numeric\"), outputs = list(answer = \"numeric\"), v = \"v1.0.3\", alias = \"manualTransmission\")apiresult <- api$manualTransmission(120, 2.8)resultprint(result$output(\"answer\")) # 0.6418125swagger <- api$swagger()cat(swagger)swagger <- api$swagger(json = FALSE)swaggerservices <- listServices(serviceName)servicesserviceName <- services[[1]]serviceNameapi <- getService(serviceName$name, serviceName$version)apiresult <- api$manualTransmission(120, 2.8)print(result$output(\"answer\")) # 0.6418125cap <- api$capabilities()capcap$swaggerstatus <- deleteService(cap$name, cap$version)statusremoteLogout() 服务支持的参数的类型和返回值类型均为以下几种: numeric integer logical character vector matrix data.frame 如果 code 是函数,只有一个值可以返回。 代码和模型区别官方文档没有明确指出 Code 和 Model 的区别。但是从代码和性能分析的角度,可以发现 Code 和 Model 有以下区别: Model 部分的代码是发布服务的时候执行的, Code 部分则是在接收网络请求的时候执行的。因为 Model 部分只包含一个模型,不可能在每次网络请求中都执行,否则求解模型的时间会很长。在网络请求中, Model 部分会被加载到 Code 部分。 Model 部分不接收网络请求的参数(即发布服务时指定的 inputs ),也不提供返回参数(即发布服务时指定的 outputs )。接收参数和返回参数的提供是在 Code 部分中进行的。 Model 部分一般是 R 对象, Code 部分一般是可执行体。 由于有这样的区别,因此使用动态模型只能在 Code 中进行,而且最好将接口设计为异步接口。 类型 来源 Code 指向 R 脚本的文件路径 字符串形式的 R 代码片段 R 函数 Model 指向 .RData 文件的路径 指向 R 脚本的文件路径,用于加载环境 模型对象,如 model = am.glm 与 Web 服务集成发布的 R 服务可以与 Web 服务集成。利用服务提供的 swagger.json 文件,可以使用 Swagger 自动生成 API 接口代码以及文档。主要流程为: 获取 Swagger 工具和文件 使用 Swagger 生成 API 库 添加认证逻辑 通过库与 API 交互或使用服务 以 mtService 与 ASP.NET 为例,首先创建 ASP.NET Web API 工程。 ASP.NET MVC 框架 MVC 是三个 ASP.NET 开发模型之一。 MVC 是用于构建 web 应用程序的一种框架,使用 MVC (Model View Controller) 设计: Model(模型)表示应用程序核心(比如数据库记录列表)。是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象在数据库中存取数据。 View(视图)对数据(数据库记录)进行显示。是应用程序中处理数据显示的部分。通常从模型数据中创建视图。 Controller(控制器)处理输入(写入数据库记录)。是应用程序中处理用户交互的部分。通常控制器从视图读取数据、控制用户输入,并向模型发送数据数据。 MVC 的这种拆分有助于我们管理复杂的应用程序,因为您能够在同一时间关注一个方面。例如,您可以在不依赖业务逻辑的情况下对视图进行设计。同时对应用程序的设计也更加容易。MVC 的这种拆分同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。 然后使用 AutoRest 或 Swagger Codegen 生成 C# 代码: 1AutoRest.exe -CodeGenerator CSharp -Modeler Swagger -Input swagger.json -Namespace Transmission 会在 swagger.json 所在文件夹下创建一个 Generate 文件夹,里面保存了生成的代码,文件结构如下: Models AccessTokenResponse.cs BatchWebServiceResult.cs Error.cs ErrorException.cs InputParameters.cs LoginRequest.cs OutputParameters.cs RenewTokenRequest.cs RenewTokenRequest.cs StartBatchExecutionResponse.cs WebServiceResult.cs IMtService1556106844.cs MtService1556106844.cs MtService1556106844Extensions.cs 每个文件都包含一个类,这些类都声明在 Transmission 命名空间中,结合 C# 命名空间取值的特点,建议将这些文件放在 ASP.NET 工程根目录的 Transmission 目录下。 另外一种方式,是将命名空间声明为 RService.Transmission ,可以将 R 服务都放置在工程根目录的 RService 目录下(如将这个服务放置在工程根目录的 RService/Transmission 目录下),便于多个接口的管理。 然后 Controllers 文件夹下,创建一个 Web API 控制器类,命名为 MtServiceController 。即可生成一个 Controller 的基本代码。 基本代码如下: 123456789101112131415161718192021222324252627282930313233343536373839using System;using System.Collections.Generic;using System.Linq;using System.Net;using System.Net.Http;using System.Web.Http;namespace TestRService.Controllers{ public class TempController : ApiController { // GET api/<controller> public IEnumerable<string> Get() { return new string[] { \"value1\", \"value2\" }; } // GET api/<controller>/5 public string Get(int id) { return \"value\"; } // POST api/<controller> public void Post([FromBody]string value) { } // PUT api/<controller>/5 public void Put(int id, [FromBody]string value) { } // DELETE api/<controller>/5 public void Delete(int id) { } }} 该基本代码提供了 GET 、 POST 、 PUT 、 DELETE 四种 HTTP 请求类型的支持。现在只需要对这些进行修改。即可。 对带参数的 GET 方法进行修改的方法 将参数修改为两个: hp 和 wt 。直接修改 public string Get(int id) 的声明: 12- public string Get(int id)+ public double? Get(double hp, double wt) 即对 GET 方法添加了两个参数,都是 double 类型。 创建 R 服务对象,指明服务基地址。 1234public double? Get(double hp, double wt){ MtService1556106844 client = new MtService1556106844(new Uri(\"http://192.168.41.49:12800\"));} 在有 SSL 证书时可以使用 HTTPS 协议。 添加服务的认证:在 Get 方法中使用 Transmission 库自带的 LoginRequest 类进行认证。 123456789101112131415161718public double? Get(double hp, double wt){ MtService1556106844 client = new MtService1556106844(new Uri(\"http://192.168.41.49:12800\")); // --- AUTHENTICATE WITH ACTIVE DIRECTORY ----------------------------------------- // Note - Update these with your appropriate values // Once authenticated, user won't provide credentials again until token is invalid. // You can now begin to interact with the operationalization APIs // -------------------------------------------------------------------------------- var loginRequest = new LoginRequest(\"admin\", \"GWmodel-Lab2018\"); var loginResponse = client.Login(loginRequest); // // SET AUTHORIZATION HEADER WITH BEARER ACCESS TOKEN FOR FUTURE CALLS // var headers = client.HttpClient.DefaultRequestHeaders; var accessToken = loginResponse.AccessToken; headers.Remove(\"Authorization\"); headers.Add(\"Authorization\", $\"Bearer {accessToken}\");} 这里的用户名 admin 和密码 GWmodel-Lab2018 是 Machine Learning Server 在启动的时候,设置的用户名和密码。 调用 R 服务并返回结果:在 Get 方法中使用 Transmission 库调用 R 服务。 12345678910111213141516171819202122public double? Get(double hp, double wt){ MtService1556106844 client = new MtService1556106844(new Uri(\"http://192.168.41.49:12800\")); // --- AUTHENTICATE WITH ACTIVE DIRECTORY ----------------------------------------- // Note - Update these with your appropriate values // Once authenticated, user won't provide credentials again until token is invalid. // You can now begin to interact with the operationalization APIs // -------------------------------------------------------------------------------- var loginRequest = new LoginRequest(\"admin\", \"GWmodel-Lab2018\"); var loginResponse = client.Login(loginRequest); // // SET AUTHORIZATION HEADER WITH BEARER ACCESS TOKEN FOR FUTURE CALLS // var headers = client.HttpClient.DefaultRequestHeaders; var accessToken = loginResponse.AccessToken; headers.Remove(\"Authorization\"); headers.Add(\"Authorization\", $\"Bearer {accessToken}\"); InputParameters inputs = new InputParameters() { Hp = hp, Wt = wt }; var serviceResult = client.ManualTransmission(inputs); return serviceResult.OutputParameters.Answer;} POST 方法、 PUT 方法的编写本质上和 GET 方法的编写没有什么区别。不同的地方在于, POST 方法中的参数通过请求体进行,而不是请求地址。参数类型可以直接声明为 InputParameters 类型,因为这个类型支持 JSON 反序列化。 12345678910111213141516171819202122232425262728namespace Transmission.Models{ using Newtonsoft.Json; using System.Linq; public partial class InputParameters { public InputParameters() { CustomInit(); } public InputParameters(double? hp = default(double?), double? wt = default(double?)) { Hp = hp; Wt = wt; CustomInit(); } partial void CustomInit(); [JsonProperty(PropertyName = \"hp\")] public double? Hp { get; set; } [JsonProperty(PropertyName = \"wt\")] public double? Wt { get; set; } }} 当请求体是如下的 HTTP 请求时,程序可以自动获取到这些参数: 1234567POST http://localhost:64620/api/MtService HTTP/1.1Content-Type: application/json{ \"hp\": 120, \"wt\": 2.8} 另外,在 GET 等方法的返回值中,如果 R 服务的返回值类型是 vector 、 matrix 或 data.frame ,那么不能直接以 XML 返回,需要调用 ToString() 函数返回。但是可以直接以 JSON 返回。方法是: 1234567891011121314151617181920212223public double? Get(double hp, double wt){ MtService1556106844 client = new MtService1556106844(new Uri(\"http://192.168.41.49:12800\")); // --- AUTHENTICATE WITH ACTIVE DIRECTORY ----------------------------------------- // Note - Update these with your appropriate values // Once authenticated, user won't provide credentials again until token is invalid. // You can now begin to interact with the operationalization APIs // -------------------------------------------------------------------------------- var loginRequest = new LoginRequest(\"admin\", \"GWmodel-Lab2018\"); var loginResponse = client.Login(loginRequest); // // SET AUTHORIZATION HEADER WITH BEARER ACCESS TOKEN FOR FUTURE CALLS // var headers = client.HttpClient.DefaultRequestHeaders; var accessToken = loginResponse.AccessToken; headers.Remove(\"Authorization\"); headers.Add(\"Authorization\", $\"Bearer {accessToken}\"); InputParameters inputs = new InputParameters() { Hp = hp, Wt = wt }; var serviceResult = client.ManualTransmission(inputs);- return serviceResult.OutputParameters.Answer;+ return Json(serviceResult.OutputParameters.Answer);} 连接池当您提前创建会话和加载依赖项时,可以快速连接到Web服务。 会话在专用于特定Web服务的池中可用,其中每个会话包括R解释器的实例和Web服务所需的依赖关系的副本。 例如,如果您为使用Matplotlib,dplyr,cluster,RevoScaleR,MicrosoftML和mrsdeploy的Web服务提前创建了10个会话,则每个会话都将拥有自己的R解释器实例以及内存中加载的每个库的副本。 具有专用会话池的Web服务从不请求来自通用会话池共享资源的连接,即使在达到最大会话时也是如此。 通用会话池仅为那些没有专用资源的Web服务提供服务。 mrsdeploy 提供以下三个函数来创建和管理会话: configureServicePool getPoolStatus deleteServicePool 创建连接池: 123456789101112131415# load mrsdeploy and print the function listlibrary(mrsdeploy)ls(\"package:mrsdeploy\")# Return a list of web services to get the service and version # Both service name and version number are requiredlistServices()# Create the session pool using a case-sensitive web service name# A status code of 200 is returned upon successconfigureServicePool(name = \"myWebservice1234\", version = \"v1.0.0\", initialPoolSize = 5, maxPoolSize = 10 )# Return status # Pending indicates session creation is in progress. Success indicates sessions are ready.getPoolStatus(name = \"myWebService1234\", version = \"v1.0.0\") 删除连接池: 12345# Return a list of web services to get the service and version informationlistServices()# Deletes the dedicated session pool and releases resourcesdeleteServicePool(name = \"myWebService1234\", version = \"v1.0.0\") 获取连接池信息: 12345678# Deletes the dedicated session pool and releases resourcesdeleteServicePool(name = \"myWebService1234\", version = \"v1.0.0\")# Check the real-time status of dedicated poolgetPoolStatus(name = \"myWebService1234\", version = \"v1.0.0\")# make sure the return status is NotFound on all computeNodes# if not, issue anthor deleteServicePool command again","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"R","slug":"R","permalink":"http://hpdell.github.io/tags/R/"}]},{"title":"通过 Metaweblog API 给 Hexo 接入客户端","slug":"hexo-metaweblog-guide","date":"2019-02-26T16:09:28.000Z","updated":"2022-04-14T16:50:55.449Z","comments":true,"path":"编程/hexo-metaweblog-guide/","link":"","permalink":"http://hpdell.github.io/编程/hexo-metaweblog-guide/","excerpt":"","text":"Hexo 搭建静态博客的问题Hexo + GitHub Pages 搭建博客的方式已经非常流行了。但是 Hexo 的写作几乎必须在电脑上进行。在 Travis 等 CI 工具的支持下,可以通过在 GitHub 页面上新建文件,编写内容,然后通过 Travis CI 持续集成,发布到 GitHub Pages 。但这个流程相当麻烦,而且在 iOS 上 GitHub 网页的那个编辑器不能直接输入中文。 解决方法有两种。 搭建动态博客。给动态博客里面加入一个编辑器,这样在手机或平板上就可以进行编辑,没有 Hexo 环境也没关系。 使用 Metaweblog API 。一些写作软件,如 MWeb 等都支持这个 API 。但是需要我们实现一套这样的 API,才能使用。 本文主要介绍如何使用 Express 实现这套接口。 依赖库以下是主要用到的依赖库: express fs-extra simple-git hexo md5 主要接口MWeb 调用了两种接口,一种是 Metaweblog API ,一种是 Blogger API 。下面分别介绍参数 Metaweblog API下面列出的是接口的参数: 接口 说明 参数 类型 参数说明 newPost 新增一篇博客 blogid String 博客 ID ,适用于一个网站很多博客的那种(如 CSDN ) username String 用户名 password String 密码 post PostContent 文章内容,具体类型详见下面的代码 getPost 获取一篇博客 postid String 文章 ID ,由 newPost 接口返回 username String password String getCategories 获取博客的分类 blogid String username String password String newMediaObject 添加多媒体文件 blogid String username String password String mediaObject MediaObject 多媒体文件,具体类型详见下面的代码 上面两个结构体的定义( Typescript 描述法): 12345678910111213141516171819202122232425interface PostContent { categories: string[]; // 分类 dateCreated: Date; // 创建日期 description: string; // 文章内容 title: string; // 文章标题 mt_keywords: string; // 标签 wp_slug: string; // 自定义 Web 链接}interface MediaObject { overwrite: boolean; bits: Buffer; // 多媒体二进制数据 name: string; // 文件名 type: string; // MIME 类型}interface Category { categoryid: string; // 分类ID description: string; // 分类描述 title: string; // 分类名称}interface NewMediaObjectReturnType { url: string; // 媒体文件访问地址} 下面是这几个接口的返回类型: 接口 返回类型 说明 newPost String 返回的是文章的 PostID getPost PostContent 返回文章的结构体 getCategories Category[] 返回的是分类的结构体数组 newMediaObject NewMediaObjectReturnType 返回的是表示媒体文件访问信息的结构体 Blogger API这里只用到了一个接口 名称 说明 参数 参数类型 参数说明 返回类型 getUserBlogs 获取博客信息 key String (不用) GetUserBlogsReturnType username String 用户名 password String 密码 接口返回信息 1234interface GetUserBlogsReturnType { blogid: string // 博客 ID blogName: string // 博客名称} 基于 express 和 xrpc 库的实现方式XML-RPC 协议的解析可以利用 xrpc 库进行实现。app.ts 的写法为 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960import * as express from 'express';import * as path from 'path';import * as favicon from 'serve-favicon';import * as logger from 'morgan';import * as cookieParser from 'cookie-parser';import * as bodyParser from 'body-parser';import * as xrpc from \"xrpc\";import { newPost, getPost, editPost, getCategories, newMediaObject } from './metaweblog';import { getUserBlogs } from './blogger';var app = express();class ExpressError extends Error { status: number;}app.use(logger('dev'));app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false }));app.use(cookieParser());app.use(express.static(path.join(__dirname, 'public')));/** * 增加的 xrpc 部分 */app.use(xrpc.xmlRpc);app.post('/RPC', xrpc.route({ metaWeblog: { newPost: newPost, getPost: getPost, getCategories: getCategories, newMediaObject: newMediaObject }, blogger: { getUsersBlogs: getUserBlogs, }}));/** * RPC */// catch 404 and forward to error handlerapp.use(function(req, res, next) { var err = new ExpressError('Not Found'); err.status = 404; next(err);});// error handlerapp.use(function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error');});module.exports = app; metaweblog.ts 的实现方式 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148import * as md5 from \"md5\";import * as fs from \"fs-extra\";import * as simplegit from \"simple-git/promise\";import * as path from \"path\";import * as moment from \"moment\";import * as Hexo from \"hexo\";import { Stream } from \"stream\";const config = require(\"./package.json\");const blog = config.meta.blogclass ExpressError extends Error { status: number; constructor(msg, code) { super(msg); this.status = code; }}const hexo = new Hexo(blog, { silent: true, safe: true});interface PostContent { categories: string[]; dateCreated: Date; description: string; title: string; mt_keywords: string; wp_slug: string;}interface MediaObject { overwrite: boolean; bits: Buffer; name: string; type: string;}export async function newPost(blogid: string, username: string, password: string, post: PostContent, publish: boolean, callback: Function) { if (blogid) { if (username.toLowerCase() === \"hpdell\" && md5(password) === \"2b759a6996d878c41bb7d56ce530d031\") { const git = simplegit(blog); if (await git.checkIsRepo()) { try { let postInfo = (await fs.readFile(path.resolve(path.join(blog, \"scaffolds\", \"post.md\")))).toString(); postInfo = postInfo.replace(\"{{ title }}\", post.title); postInfo = postInfo.replace(\"{{ date }}\", moment(post.dateCreated).format(\"YYYY-MM-DD HH:mm:ss\")); if (post.categories && post.categories.length) { postInfo = postInfo.replace(\"categories:\", `categories: ${post.categories[0]}`); } if (post.mt_keywords) { let tags = post.mt_keywords.split(\",\"); let tagInfo = tags.map(item => ` - ${item}`).join(\"\\n\"); postInfo = postInfo.replace(\"tags:\", `tags:\\n${tagInfo}`); } let content = postInfo + post.description; let postPath = path.resolve(path.join(blog, \"source\", \"_posts\", `${post.wp_slug}.md`)); try { await fs.writeFile(postPath, content); try { await publishToGitHub(git, path.join(\"source\", \"_posts\", `${post.wp_slug}.md`)); callback(null, post.wp_slug); } catch (error) { callback(new ExpressError((error as Error).message, 500)); } } catch (error) { callback(new ExpressError(\"Internal Server Error: Cannot write post file.\", 500)); } } catch (error) { callback(new ExpressError(\"Internal Server Error: Cannot read scaffolds.\", 500)); } } else { callback(new ExpressError(\"Inernal Server Error: Not a hexo repo.\", 500)); } } else { callback(new ExpressError(\"Username or Password is wrong.\", 500)); } } else { callback(new ExpressError(\"Blog id is required.\", 500)); }}export async function getPost(postid: string, username: string, password: string, callback: Function) { try { await hexo.load() let posts = hexo.locals.get(\"posts\").filter((v, i) => v.title === postid).toArray(); if (posts && posts.length) { let post = posts[0]; let postStructure: PostContent = { categories: post.categories, dateCreated: post.date.toDate(), description: post.content, title: post.title, mt_keywords: post.tags.join(\",\"), wp_slug: path.basename(post.path) }; callback(null, postStructure) } } catch (error) { callback(new ExpressError(\"Post not found\", 500)); }}export function getCategories(blogid: string, username: string, password: string, callback: Function) { callback(null, config.meta.categories.map((item: string) => { return { categoryid: item, description: item, title: item } }));}export async function newMediaObject(blogid: string, username: string, password: string, mediaObject: MediaObject, callback: Function) { if (blogid) { if (username.toLowerCase() === \"hpdell\" && md5(password) === \"2b759a6996d878c41bb7d56ce530d031\") { let imgPath = path.join(blog, \"source\", \"assets\", \"img\", mediaObject.name); try { await fs.writeFile(imgPath, mediaObject.bits); callback(null, { url: \"/\" + [\"assets\", \"img\", mediaObject.name].join(\"/\") }) } catch (error) { callback(new ExpressError(\"Writefile Wrong.\", 500)); } } else { callback(new ExpressError(\"Username or Password is wrong.\", 500)); } } else { callback(new ExpressError(\"Blog id is required.\", 500)); }}async function publishToGitHub(git: simplegit.SimpleGit, postPath) { try { let imgDirPath = path.join(\"source\", \"assets\", \"img\", \"*\"); await git.add([postPath, imgDirPath]); await git.commit(`Add Post: ${path.basename(postPath)}`); await git.pull(\"origin\", \"master\", { \"--rebase\": \"true\" }); await git.push(); } catch (error) { throw new Error(\"git error\"); }} 这个实现还比较粗糙,为了简单起见, getPost 还是调用了 Hexo 的接口。分类部分还是用的 package.json 文件中确定的几种类型。对 blogid 也没有做判断。 blogger.ts 的实现方式 1234export function getUserBlogs(key: string, username: string, password: string, callback: Function) { console.log('getuserblogs called with key:', key, 'username:', username, 'and password:', password); callback(null, [{ blogid: 'hpdell-hexo', blogName: 'HPDell 的 Hexo 博客', }]);} 这里这个实现也是为了简单起见,直接返回了固定的名称和 ID 。 至此,这个接口就算是基本实现了。 效果查看 我的博客 里面有两篇博文 测试发布到 GitHub 测试通过 Metaweblog API 发图片 即可查看效果。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"TypeScript","slug":"TypeScript","permalink":"http://hpdell.github.io/tags/TypeScript/"},{"name":"Node.js","slug":"Node-js","permalink":"http://hpdell.github.io/tags/Node-js/"},{"name":"博客相关","slug":"博客相关","permalink":"http://hpdell.github.io/tags/博客相关/"}]},{"title":"测试发布到 GitHub","slug":"test-metaweblog-api","date":"2019-02-26T13:10:55.000Z","updated":"2022-04-14T16:50:55.573Z","comments":true,"path":"其他/test-metaweblog-api/","link":"","permalink":"http://hpdell.github.io/其他/test-metaweblog-api/","excerpt":"","text":"测试一篇测试博客测试通过 MetaWeblog API 发布博客。","categories":[{"name":"其他","slug":"其他","permalink":"http://hpdell.github.io/categories/其他/"}],"tags":[{"name":"测试","slug":"测试","permalink":"http://hpdell.github.io/tags/测试/"}]},{"title":"测试通过 MetaWeblog API 发图片","slug":"test-metaweblog-api-image","date":"2019-02-26T13:10:55.000Z","updated":"2022-04-14T16:50:55.573Z","comments":true,"path":"其他/test-metaweblog-api-image/","link":"","permalink":"http://hpdell.github.io/其他/test-metaweblog-api-image/","excerpt":"","text":"测试一篇测试博客测试通过 MetaWeblog API 发布博客。 ","categories":[{"name":"其他","slug":"其他","permalink":"http://hpdell.github.io/categories/其他/"}],"tags":[{"name":"测试","slug":"测试","permalink":"http://hpdell.github.io/tags/测试/"}]},{"title":"自己动手写动态博客","slug":"dynamic-blog","date":"2019-02-24T20:34:46.000Z","updated":"2022-04-14T16:50:55.429Z","comments":true,"path":"编程/dynamic-blog/","link":"","permalink":"http://hpdell.github.io/编程/dynamic-blog/","excerpt":"","text":"我自己动手写了一个基于 Quasar Express 的动态博客,使用 TypeScript 编写。博客代码放在 GitHub 上,目前是私有库状态。 后端仓库: HPDell/blog-zone 前端仓库: HPDell/blog-zone-views 起初做这个动态博客的原因是,过年想用手机发一篇博客。 虽然有 Travis 持续集成,已经为用手机写提供了可能,但整个流程走下来,过程还是太复杂了, 而且 GitHub 的编辑器 iOS 不能直接输入中文,导致我只能先在备忘录里面写好。 正好趁着练一下以前没有涉足过的技术。(目前博客已经部署,但是由于是自己家的服务器,不太敢放链接出来。) 依赖库前端主要用到的依赖库有: Quasar 、 Webpack 和 Vue 系全家桶 MavonEditor :Markdown 编辑器(由于需要做必要的改造,因此将源仓库克隆,使用克隆后的仓库) marked :渲染 Markdown Prism.js :代码高亮 MathJax :渲染数学公式 abcjs :渲染乐谱 除此之外还有 jquery 、 md5 、 moment 、 markdown-loader 、 html-loader 等 后端主要用到的依赖库有: Express Typeorm Moment.js Sqlite3 JsonWebToken uuid 部署方法后端的部署后端的安装比较简单。克隆仓库后,使用如下流程进行部署 12345678yarn global add typescript # 如果没有 TypeScript 环境yarntsc# 直接启动 Node 进程node app.js# 或推荐使用 PM2 管理进程pm2 start app.js --name blog-zone 部署完后,可以通过调用一次 POST /login/register 接口,来为数据库添加一个管理员用户。 由于没有开放注册接口,因此对通过接口进行注册进行了限制。 如果想修改这个限制到 ${maxUserNumber},对 /routes/login.ts 进行修改: 12345678910111213141516171819202122232425router.post(\"/register\", async function (req: Request, res: Response) { const connection = getConnection(); try { let userList = await connection.getRepository(User).find();- if (userList.length < 1) { + if (userList.length < ${maxUserNumber}) { let userInfo = new User(); userInfo.name = req.body.username; userInfo.password = req.body.password; userInfo.description = req.body.description; try { let user = await connection.manager.save(userInfo); res.json(user); } catch (error) { console.log(error); res.sendStatus(500); return; } } } catch (error) { console.log(error); res.sendStatus(500); return; }}) 日后会将这个配置加入到 package.json 中。 前端的部署前端的安装也比较简单。克隆仓库后,按如下流程部署 123yarn global add quasar-cliyarnquasar build 将 dist 文件夹生成的文件用服务器(如 Nginx)进行代理,即可访问。 剩下的就是使用 Nginx 对后台进行反向代理了。配置脚本如下 1234567891011121314151617server { listen 80; server_name $domain; location /api/ { proxy_pass http://localhost:3000/api/; } location /login/ { proxy_pass http://localhost:3000/login/; } location / { root $pathToDist; }} 主要配置选项后端的数据库是可选的,参考 Typorm 的配置 。 默认采用的是 SQLite 数据库,数据库文件是根目录的 database.db 文件。 前端主要有以下几个配置: package.json 文件 meta 属性 owner : 博客显示的名称(即显示 ${owenr} 的博客 ,${owner} 的博文 等) description : 博客显示的简介 /src/components/welcome.md 文件:这个文件记录了主页显示的内容。将来可能移动到专门的地方,或者放在服务器中。 支持的内容博客支持以下内容 微文:一般用于撰写小段独立文字,配图最多 9 张,而且也是独立于文字显示的。 博文:一般用于长文写作,需要提供标题。 微文和博文都支持 Markdown 语法。本博客对 Markdown 的支持进行了扩展,拥有以下功能 代码:语法高亮、行号、显示语言。 数学公式:支持 LaTeX 格式编写的数学公式。 乐谱:支持 ABC 格式书写的乐谱,同时显示乐谱和 MIDI 音频。 注意:微文支持 Markdown 语法,表明微文中可以插入图片。单一般不建议在微文文字中插入图片。 这样插入的图片不支持点击查看大图。 如果你喜欢本项目,欢迎在您心理点个赞。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"TypeScript","slug":"TypeScript","permalink":"http://hpdell.github.io/tags/TypeScript/"},{"name":"网页开发","slug":"网页开发","permalink":"http://hpdell.github.io/tags/网页开发/"},{"name":"Node.js","slug":"Node-js","permalink":"http://hpdell.github.io/tags/Node-js/"}]},{"title":"《爱乐之城》的“过度”解读","slug":"la-la-land","date":"2019-02-17T17:42:29.000Z","updated":"2022-04-14T16:50:55.477Z","comments":true,"path":"随笔/la-la-land/","link":"","permalink":"http://hpdell.github.io/随笔/la-la-land/","excerpt":"《爱乐之城》是近几年来我最喜欢的电影,没有之一。不论是两人执着追求梦想的热情,还是在天上共舞的浪漫,或是两人相爱不能相守的遗憾,都令我非常感动。动听的歌曲也让这部电影怎么也看不腻。","text":"《爱乐之城》是近几年来我最喜欢的电影,没有之一。不论是两人执着追求梦想的热情,还是在天上共舞的浪漫,或是两人相爱不能相守的遗憾,都令我非常感动。动听的歌曲也让这部电影怎么也看不腻。 电影表达的主旨其实很简单,但我想要说的太多太多,非常凌乱杂碎。每一次看这部电影,都有新的感悟,新的理解。但不管想说多少东西,还是都要先从故事开始。 冬、春、夏、秋、冬这部电影的一个特点,是通过章节式的标题“冬、春、夏、秋、冬”组织故事素材,推进故事的发展。冬、春、夏、秋、冬以及五年之后,提示观众故事的进程。这简单的看是一种时间的线索,表明男女主从相识相知,到相忘于江湖,最后又重逢,经历了四季五年。但女主全片都穿的是短袖裙子,怎么可能冬天也是这么清凉的夏装呢?洛杉矶的冬天也没有那么暖和。 可能这里更要表示的是男女主之间的感情状态。 男主女主一开始不认识,在立交桥上互竖中指;米娅碰巧走到小塞工作的餐厅,正要对他说你的音乐很好听,却差点被愤怒的小塞撞到。这是两人感情的寒冬。 女主受室友邀请参加泳池酒会,又碰巧和小塞遇到,两人就会后独行,虽然嘴上说着看不上对方,实际早已心动。米娅逃离了现男友的聚会,来到电影院和小塞在一起,正是两人感情的春天到了。 在离开现男友的聚会之前,米娅听到餐厅音响里传来小塞弹琴的声音。我觉得这是个绝妙的细节。刚开始看的时候,觉得是不是米娅听错了,甚至还觉得不可思议。后来想想,不论广播里放的是什么,米娅都会听成小塞的琴声,绝妙刻画了米娅对小塞迫不及待的爱。 而米娅在筹备自己的独角戏,正巧小塞在家里为米娅准备“惊喜”,结果两人却大吵一架。米娅独角戏演出失败,告别了洛杉矶,自己回到老家。虽然小塞后来找到了米娅,接她去面试,两人在公园里互说“我会永远爱你”的,但一切已经无可挽回,况且米娅还要到巴黎去拍戏。两个人的关系已经秋过冬至。 直到五年后,他们的感情仍然是“冬眠”着的。米娅虽然来到了小塞的俱乐部,小塞为她弹奏了音乐,两人共同幻想了他们在一起的美好生活。但是米娅走了,虽然有回头对视,但他们已经各自有各自的生活,已经走远了。于是米娅微笑后扭头走了,小塞微笑后继续下面的演奏。 经过一年的耕耘,秋天该是收获的季节,冬天则是人们休整、享受劳动果实的时间。但米娅和小塞之间没有留下果实,也没有什么好享受的。要享受只能在脑海中幻想。他们的感情为什么就这样结束了?为什么及时两人还爱着却分开了? 这可能还要从这部电影探讨的梦想、爱情和现实的角度去思考。 梦想、爱情、现实在影片的开头,那段长镜头歌舞,有一句歌词:“我将他留在圣达菲西边灵缇车站,雨季年华十七岁,少年情谊尤显真,但我还是做了该做的事,因为我知道,夏日夜,我们陷进座椅,灯光转暗,音乐和机器交织的多彩世界,银幕召唤着我跃动在每一幕。”表达的是为了梦想放弃爱情;另一句歌词:“即便屡次遭拒绝,即便钱财皆散尽,我只需要布满灰尘的麦克风,还有耀眼的霓虹灯。”表达的是不顾现实的阻碍去追求梦想。电影可能要告诉我们的是,为了梦想,什么都值。 但有趣的是,梦想和爱情好像谁的斗不过现实,他们都是美好的东西,却都可能是不切实际的东西。但正是我们的梦想和爱情,让我们的生活变得丰富多彩,与众不同。 米娅的梦想,是成为一名演员;小塞的梦想,是拯救爵士乐。米娅演独角戏、小塞开俱乐部是他们实现梦想的途径。他们互相鼓励着对方实现梦想,在对方迷茫、犹豫、退缩的时候,给对方以力量。这是他们两个之前的爱情的可贵之处,真正的同甘共苦。而现实是,他们开始只能挤在二三十平米的小房子里,两人都没有稳定的工作,米娅的独角戏还要自己贴钱做。 所以小塞向现实妥协了。答应了好友,加入了乐队,去演奏他不那么喜欢的音乐。 可以说米娅其实是踩着小塞的梦想去实现的梦想,虽然最后她对于结果也不满意。也许对于小塞来说,这种牺牲,才是伟大的,也是值得的。而米娅却指责小塞放弃了梦想,想要让小塞离开乐队,她没有意识到,甚至几乎完全否定了小塞加入乐队的价值,这可能是小塞发火的原因。而小塞说“也许你只是喜欢落魄时的我,因为那样能让你有优越感”,确实也否定了米娅对小塞梦想的关怀,米娅可能正是怀着一种“我这么说都是为你好你却这样子说我”的心态,离开了小塞。 最后米娅也要向现实妥协了,她要回到学校,要放弃梦想。但小塞找到她,让他去试镜,她去了,也成功了。但小塞的爱情,因此也终结了。 于是米娅相当于是踩着小塞的梦想和爱情实现了自己的梦想。还好小塞最后还是开办了俱乐部,而且办的还不错。米娅也喜欢上了爵士乐,相信她再听到爵士乐的时候,能想起曾经那个痴迷爵士乐的小塞。 对于小塞和米娅来说,可能现实就是这么残酷,人总想什么都要,但往往并不能十全十美。他们也算是实现梦想了,却放弃了自己的爱情,放弃了那个一直支持着自己的人,值吗? 如果重来 还会再跳一次电影告诉我们,值! 米娅的姑姑,光着脚,跳进了塞纳河。冰冷的河水让她得了重感冒,很快就去世了。但姑姑一路微笑着对米娅说:如果重来她还会再跳一次。正是那种宛如没有上限的天空、画框中的落日的感觉,让她觉得这一切都值了。 到了这里,电影揭示了想表达的主旨,简单地说就是为了梦想什么都值。 小塞不是不能追回米娅,米娅也不是真的在生小塞的气(她生自己的气更多一些),但米娅为了梦想要去巴黎,小塞为了梦想要开俱乐部,他们就此散了。如果让他们重来,他们还是会这样选择吧。相信他们在未来,即是彼此只能回忆,也很甜了。 繁星之城小塞第一次唱《City of Stars》,曲调较低,歌词也显得迷茫。“这是美妙缘分的开端吗,抑或又一个我无法实现的梦?”可以看出他对于他和米娅的感情没有什么把握,对于自己的梦想也没什么把握。 小塞第二次唱《City of Stars》,曲调较高,米娅接的词也更积极,“一次深情对望,点亮整片天空,打开世界,眼花缭乱,一句暖心话语,我在,别怕。我不在乎将去向何方,只因这般疯狂已然足矣。”这也符合他们的状态,为了梦想不顾一切拼尽全力。 他们第二次唱的《City of Stars》,和电影中的另外两首歌曲《Another Day of Sun》和《The Fools Who Dream》共同揭示了主题。 有了梦想,即是闯了祸,即是穷困潦倒,也没什么,因为我们不会有遗憾。 当最后这几首曲子,又一次出现在小塞和米娅的共同幻想中,指示出他们完全有可能的另外一条人生轨迹,不禁令人唏嘘不已。在这个幻想中,小塞直接拒绝了老同学的邀请,为了帮助米娅实现梦想拼尽全力,而他也能在俱乐部里享受自己喜欢的爵士乐,他们还能一直在一起,收拾屋子,生个孩子……一切就是这么美好,但似乎这一切都毁在了小塞那个向现实妥协的决定上。从这个角度来看,这段共同幻想可能小塞想得更多,后悔的也更多。 其他有人批评本片其实不过是白人式的美国梦。片中米娅和小塞在工作中总是出岔子,其实是对契约精神的不尊重和对他人的不负责。我表示确实认同,包括米娅相当于踩着小塞的梦想和爱情实现自己梦想这点,我也觉得不应该。 但是片中两人对梦想的无条件的肯定和执着的对求,相互之间的鼓励和支持,以及本可以有一个美好的未来最后却彼此错过,更打动我。尤其是本片主旨中对梦想价值肯定,即使看起来很蠢,或闯了很多祸,也不否认梦想的意义,依然鼓励人们克服现实困难追求梦想,差点让我热泪盈眶。有多少人曾有梦想,最后在身边人的不解和劝阻中放弃了,最后活成别人了呢? Here’s to the ones who dream. Foolish as they may seem. Here’s to the hearts that ache. Here’s to the mess we made.","categories":[{"name":"随笔","slug":"随笔","permalink":"http://hpdell.github.io/categories/随笔/"}],"tags":[{"name":"影评","slug":"影评","permalink":"http://hpdell.github.io/tags/影评/"}]},{"title":"Node.js 实现阿里云域名的动态解析","slug":"nodejs-ddns","date":"2019-02-16T20:42:52.000Z","updated":"2022-04-14T16:50:55.561Z","comments":true,"path":"编程/nodejs-ddns/","link":"","permalink":"http://hpdell.github.io/编程/nodejs-ddns/","excerpt":"家用宽带申请公网 IP 往往是动态的公网 IP ,会经常变动,只能借助域名和 DDNS 使服务器可以随时被访问。 使用 Node.js 做 DDNS 也很简单,借助阿里云的 OpenAPI Explorer 可以做一个简单实现。","text":"家用宽带申请公网 IP 往往是动态的公网 IP ,会经常变动,只能借助域名和 DDNS 使服务器可以随时被访问。 使用 Node.js 做 DDNS 也很简单,借助阿里云的 OpenAPI Explorer 可以做一个简单实现。 在线查找域名解析记录的 RecordID这步没有必要每次都用代码实现,因为域名解析记录的 ID 在修改的过程中是不会发生变化的。 所以直接通过 OpenAPI Explorer 来在线查询 RecordID 即可。 首先在 OpenAPI Explorer 中选择“云解析”的接口,找到 DescribeDomainRecords 接口,填入指定的参数, 点击“发起调用”,即可看到所有的域名解析记录。 参数栏中, DomainName 一处填写要修改解析记录的域名。右侧找到要修改的域名记录,记下 RecordID 。 动态修改域名解析记录这里需要借助以下三个接口: 淘宝查询公网 IP 地址的接口。地址是 http://www.taobao.com/help/getip.php 阿里云 DescribeDomainRecordInfo 接口 阿里云 UpdateDomainRecord 接口 淘宝的接口用 Axios 库调用,阿里云的两个接口可以通过阿里云提供的 SDK 调用。 包的引入和对象声明这部分引入包,并声明一些变量,以便后面调用。 参数 含义 newIP 新的 IP 地址 client 阿里云 SDK 对象 describeDomainRecordInfoParams 调用 DescribeDomainRecordInfo 接口的参数 updateDomainRecordParams 调用 UpdateDomainRecord 接口的参数 requestOption 阿里云 SDK 请求配置 1234567891011121314151617181920212223242526const Core = require('@alicloud/pop-core');const axios = require('axios').default;const moment = require('moment');var newIP = \"127.0.0.1\";var client = new Core({ accessKeyId: '<accessKeyId>', accessKeySecret: '<accessKeySecret>', endpoint: 'https://alidns.aliyuncs.com', apiVersion: '2015-01-09'});var describeDomainRecordInfoParams = { \"RecordId\": \"<RecordId>\"};var updateDomainRecordParams = { \"RecordId\": \"<RecordId>\", \"RR\": \"@\", \"Type\": \"A\"}var requestOption = { method: 'POST'} 查询当前公网 IP 地址调用淘宝接口进行查询,结果用正则表达式进行匹配。 1234567891011axios.get(\"http://www.taobao.com/help/getip.php?t=\" + moment().format(\"x\")).then((response) => { if (response.data) { var matched = /(\\d{1,3}\\.){3}\\d{1,3}/.exec(response.data); if (matched && matched.length) { newIP = matched[0]; } // ......}).catch((reason) => { console.error(\"Get public IPv4 error:\", reason); process.exit(1);}); 匹配IP地址的正则表达式的写法不止 /(\\d{1,3}\\.){3}\\d{1,3}/ 这一种。 可以用更简化但匹配范围更广的写法,或用更严格的写法,只要达到目的即可。 查询域名解析记录中记录的 IP 地址调用阿里云接口,将结果与当前公网 IP 进行对比。 1234567client.request(\"DescribeDomainRecordInfo\", describeDomainRecordInfoParams, requestOption).then((result) => { var oldIP = result.Value; // ......}).catch((reason) => { console.error(\"DescribeDomainRecordInfo error:\", reason); process.exit(1);}) 修改解析记录如果当前公网 IP 发生了变化,那么调用接口修改解析记录。 123456789101112if (oldIP != newIP) { updateDomainRecordParams = { ...updateDomainRecordParams, \"Value\": newIP }; client.request(\"UpdateDomainRecord\", updateDomainRecordParams, requestOption).then(() => { process.exit(0); }).catch((reason) => { console.error(\"UpdateDomainRecord error:\", reason); process.exit(1); });} 修改成功可以做一个输出,也可以不要。 如果修改前后解析记录相同,则阿里云接口会返回一个字段 Message , 值为 The DNS record already exists 。但是对于整个过程没有什么影响,因此没有对此错误进行处理。 但是为了避免频繁调用 API ,还是应该在调用接口发现 IP 地址发生变化之后再调用修改解析记录的接口。 完整代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657const Core = require('@alicloud/pop-core');const axios = require('axios').default;const moment = require('moment');var newIP = \"127.0.0.1\";var client = new Core({ accessKeyId: '<accessKeyId>', accessKeySecret: '<accessKeySecret>', endpoint: 'https://alidns.aliyuncs.com', apiVersion: '2015-01-09'});var describeDomainRecordInfoParams = { \"RecordId\": \"<RecordId>\"};var updateDomainRecordParams = { \"RecordId\": \"<RecordId>\", \"RR\": \"@\", \"Type\": \"A\"}var requestOption = { method: 'POST'}axios.get(\"http://www.taobao.com/help/getip.php?t=\" + moment().format(\"x\")).then((response) => { if (response.data) { var matched = /(\\d{1,3}\\.){3}\\d{1,3}/.exec(response.data); if (matched && matched.length) { newIP = matched[0]; } client.request(\"DescribeDomainRecordInfo\", describeDomainRecordInfoParams, requestOption).then((result) => { var oldIP = result.Value; if (oldIP != newIP) { updateDomainRecordParams = { ...updateDomainRecordParams, \"Value\": newIP }; client.request(\"UpdateDomainRecord\", updateDomainRecordParams, requestOption).then(() => { process.exit(0); }).catch((reason) => { console.error(\"UpdateDomainRecord error:\", reason); process.exit(1); }); } }).catch((reason) => { console.error(\"DescribeDomainRecordInfo error:\", reason); process.exit(1); }) }}).catch((reason) => { console.error(\"Get public IPv4 error:\", reason); process.exit(1);}); 基于 Docker 服务化在威联通等一些 NAS 系统上,系统本身提供了获取 IP 地址的功能,而且支持向 DDNS 服务器发送请求以更改 IP 地址。 一些路由器固件中已经内置了修改阿里云 DNS 解析记录的功能,但是威联通自带的 DDNS 并不支持直接修改阿里云 DNS。 但是威联通支持通过自定义 DDNS 请求,请求中可通过 %IP% 字符串代替当前 IP 地址,例如这样的一个请求: 1https://ddns.ddd.qnap.net/?hostname=%HOST%&username=%USER%&password=%PASS%&IP=%IP% 因此我们可以将上述 DDNS 代码通过构建 Docker 镜像发布成服务,使威联通 NAS 通过该服务修改 DNS 解析记录。 服务接口实现要实现一个网络服务接口,方法很多。因为对 Node.js 比较熟,笔者这里采用了 Express 服务器框架。 总体来说,只需要写一个路由即可。 阿里云 API 所需要的 ACCESSKEY_ID 和 ACCESSKEY_SECRET 通过环境变量进行设置。 为了支持多个域名的 DDNS 而不需要部署过多的 Docker 容器,RecordID、Type、RR、IP 等参数都从请求地址中获取。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465// routes/ddns.jsconst Core = require('@alicloud/pop-core');const axios = require('axios').default;const moment = require('moment');const express = require(\"express\");var router = express.Router();function updateDNS(ip, record, type, rr) { var client = new Core({ accessKeyId: process.env.ACCESSKEY_ID, accessKeySecret: process.env.ACCESSKEY_SECRET, endpoint: 'https://alidns.aliyuncs.com', apiVersion: '2015-01-09' }); var describeDomainRecordInfoParams = { \"RecordId\": record }; var requestOption = { method: \"POST\" }; return new Promise((resolve, reject) => { client.request(\"DescribeDomainRecordInfo\", describeDomainRecordInfoParams, requestOption).then((result) => { var oldIP = result.Value; if (oldIP != ip) { var updateDomainRecordParams = { \"RecordId\": record, \"RR\": rr, \"Type\": type, \"Value\": ip }; client.request(\"UpdateDomainRecord\", updateDomainRecordParams, requestOption).then(() => { resolve(\"OK\"); }).catch((reason) => { reject(\"UpdateDomainRecord error: \" + reason); }); } }).catch((reason) => { reject(\"DescribeDomainRecordInfo error: \" + reason); }); });}router.get('/qnap', function (req, res) { var query_keys = [\"ip\", \"record\", \"type\", \"rr\"]; var query_keys_check = query_keys.every(item => (item in req.query)); if (!query_keys_check) { console.error(\"Missing:\", query_keys.filter(item => !(item in req.query)).join(\",\")); res.sendStatus(500); return; } console.log(req.query); var newIP = req.query.ip; var recordID = req.query.record; var recordType = req.query.type; var recordRR = req.query.rr; updateDNS(newIP, recordID, recordType, recordRR).then((response) => { console.log(response); res.sendStatus(200); }).catch((reason) => { console.error(reason); res.sendStatus(500); });})module.exports = router; 当然这里也可以添加一个自动获取 IP 地址并更新 DNS 解析记录的接口。 实现服务器后,只需要写一个简单的 Dockerfile 即可。 12345678FROM node:10COPY . /srvWORKDIR /srvRUN npm installEXPOSE 3000CMD [ \"node\", \"bin/www\" ] 具体实现请参考 Github 仓库。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"Node.js","slug":"Node-js","permalink":"http://hpdell.github.io/tags/Node-js/"}]},{"title":"Common Connotations of Western Fantasy Movies","slug":"fantasy-movies","date":"2019-02-11T22:30:11.000Z","updated":"2022-04-14T16:50:55.433Z","comments":true,"path":"随笔/fantasy-movies/","link":"","permalink":"http://hpdell.github.io/随笔/fantasy-movies/","excerpt":"Hello everyone, my topic is “Common Connotation of Western Fantasy Movies”. I will give my speech from these six aspects.","text":"Hello everyone, my topic is “Common Connotation of Western Fantasy Movies”. I will give my speech from these six aspects. What is fantasy movie? From Wikipedia and Baidu Encyclopedia I found these definitions. Fiction, Humanity are two key words in the definitions. So fantasy movies are some fictional movies contains magical elements which reflect humanity. I think fantasy movies are more romanticism than realism, so Dream of the Red Chamber and some other works is not fantasy works. There are many famous fantasy movies, such as The Lord of the Ring, Harry Potter, Game of Thrones, Coco, The Chronicles of Narnia, Pirates of the Caribbean, Twilight and so on. Among these movies, I found four common connotations: Fantasy movies reveals people’s understanding of the birth and operation of the world. Fantasy movies reflects people’s desire to change nature and break away from the laws of nature. Fantasy movies conveys the consensus that we must struggle to master destiny and control nature. Fantasy movies reflects the thinking of people under the conditions of fantasy. I will detail describe these points. First, Visions of Our WorldMany fantasy movies described a grand world and their world are some visions of the real world exists in a certain historical period. These pictures are some components of J.R.R. Tolkien’s world, contains wizards, elves, hobbits, dwarfs, Orcs, Nazgul and others. These are components of Jorge Martin’s world, such as dragons, white walkers, gods and so on. Some fantasy movies also described the world of the death. Where we go after we dead is a question many people curious. Coco gave us a beautiful answer. After we dead we will go to a wonderful world and we can go back to the real world on dead festival. “The real death is that no one in the world remembers you.” We need to remember our relatives, don’t we? In the real world, our mind and emotion cannot cause substance change. But if in fantasy world? They can. In Harry Potter the love is a powerful power to protect little Harry. Dumbledore can use spells without a wand. This reflect the opinions of idealists. Second, Breaking the Laws of NatureThe laws of nature limit us in many ways. We can’t fly, dive, live forever, be invisible, be too high or speak to dead ones. So fantasy movies reflect people’s desire for these ability. In The Load of The Ring and The Hobbit, elves are all beautiful. The have beautiful appearance and they can keep their hair style after a long fight. And they are much better at archery than men. In many fantasy movies, giants are very common. In Jack and the Beanstalk giants are bad guys but in Harry Potter Giants are usually good guys. Our desire for growing taller can come true in fantasy movies. In real world, we can never know what will happen in the future. What if we can know? What will happen? So prophecies are very important components in fantasy movies. Many prophecies in Game of Thrones will presage characters’ fate and story plot. “Everyone wants to know their future until they know their future. You won’t like the answers”. In Harry Potter, when Voldemort know the prophecy that the boy born in the end of July will kill him, he started killing others. Third, Controlling the Nature and PowerMagic is powerful, but we still need to practice, like any other skill in real life such as boxing, swimming and singing. We need to practice hard to be skilled. If our level is higher than many other wizards, we will become a legacy and our power will be stronger. The power of nature is endless. May be earthquake, typhoon and flood make human beings afraid of nature and we desire to seek methods to control it. In fantasy movies nature’s power is much more powerful than human or devils. The light can defeat orcs, water can defeat Nazgul. Elves are those who can control the nature, and some of them can speak to plants and animals. If we forge a sword with the moon light, the sword will be more powerful. Last, Views of Human NatureHow temptation will change us? In J.R.R Tolkien’s world, men are more desire power. Sméagol killed his mate for the Mater Ring. Isildur abandoned destroy the load of the ring and take it which made millions of soldier’s deaths useless. Frodo could not resist the temptation of the Load of The Ring and he abandoned too. And what we can do to make ourselves live forever? Tom Riddler made seven Horcruxes to ensure he can live forever. To make one Horcrux he needs to kill one person and he killed seven people. “Isn’t it bad enough to consider killing one person?” In conclusion If from the perspective of science and rationality, magic and monsters do not exist, but the world is not only scientific and rational. Fantasy movies provide a way to escape our ordinary everyday life. This is the charm of fantasy movies. It reflects the uniqueness of human beings and reflects our ability to think and experience beyond rationality. Although the fantasy film can’t give us the facts, it is a kind of reality because it has the vitality of myth. Fantasy movie is a way of describing reality. Fantasy movies portray reality with illusory content, where everything can happen, and everything is traceable. Movie is essentially the biggest fantasy. That’s all. Thank you.","categories":[{"name":"随笔","slug":"随笔","permalink":"http://hpdell.github.io/categories/随笔/"}],"tags":[{"name":"电影","slug":"电影","permalink":"http://hpdell.github.io/tags/电影/"}]},{"title":"2019新年","slug":"2019-new-year","date":"2019-02-05T00:25:00.000Z","updated":"2022-04-14T16:50:55.357Z","comments":true,"path":"随笔/2019-new-year/","link":"","permalink":"http://hpdell.github.io/随笔/2019-new-year/","excerpt":"祝大家新年快乐。","text":"祝大家新年快乐。 这一年,失去的太多,想要的太多,放弃掉的也太多。窗外偶尔传来的鞭炮声,似乎在阵阵诉说:“苟活于世上的人啊,你太失败了。” 告别了本科,迎来了研究生。首先迎接我的就是满满的课表。我也不知道我为什么到了研究生还要上这么多课,我为什么非要这一个学期上这么多课,只是这样可以安心一些吧,强行给自己一个调整的时机。依然还没做好接受研究生的学习工作方式,甚至有时看着机房的电脑会心生厌恶。希望新年能更投入,更专心吧。 新年都希望快乐。我想人可能要得到自己真正想要的才能真正的快乐。这种快乐不是看电影唱歌喝酒吹牛这种短暂的快乐可比的。哈利真正的快乐是见到自己的父母,即使只是在镜子里也无比欣喜。那我呢?实现成为科学家的梦想吗?发论文出国读博吗?是的。在理论方面的成果是我最想要的吧。 想要这种快乐只怕是祝福不来的。自己要更坚定,更努力。一心一意地去追求,收获那份真正的喜悦。而这过程之中的任何短暂的快乐,都可能是收获那份真正喜悦的绊脚石。 2018年的心理状态太差了,有的时候忍不住就大动肝火,而又有的时候忍不住痛哭流涕。注销了微博和IT之家的账号,关掉了朋友圈和空间,知乎回答也删除了,两篇简书也删掉了。对生活的一切失去兴趣,完全找不到不到快乐的方向,似乎是个可悲的事。我也和谈了两年半已经1000多天的女朋友分手了。 放下过去,收拾心情,努力奋斗!我一定要收获那一份真正的快乐。","categories":[{"name":"随笔","slug":"随笔","permalink":"http://hpdell.github.io/categories/随笔/"}],"tags":[{"name":"随笔","slug":"随笔","permalink":"http://hpdell.github.io/tags/随笔/"}]},{"title":"《逃避虽可耻但有用》观后感","slug":"nigeru-wa-haji-daga-yaku-ni-tatsu","date":"2019-01-22T14:09:39.000Z","updated":"2022-04-14T16:50:55.525Z","comments":true,"path":"随笔/nigeru-wa-haji-daga-yaku-ni-tatsu/","link":"","permalink":"http://hpdell.github.io/随笔/nigeru-wa-haji-daga-yaku-ni-tatsu/","excerpt":"“在匈牙利有这么一句谚语,逃避可耻却有用。选择后退不也很好吗,即使逃避很可耻,但更重要的是活下去。”","text":"“在匈牙利有这么一句谚语,逃避可耻却有用。选择后退不也很好吗,即使逃避很可耻,但更重要的是活下去。” 很早就被安利了这部剧,最近看完了。不是觉得新垣结衣好看才觉得这部剧好看,否则早就吹爆这部剧了。 这部剧刻画了一个极端的程序员的形象,并一点一点描写这样的程序员在面对生活上的改变。 同时也刻画了一个女权的家庭主妇形象,并一步一步寻找对家庭主妇价值,提出对待家庭主妇的生产力问题的思考。 新垣结衣给你们(手动滑稽),我最关注的还是这个“程序员”。 无论从哪个角度来说,新垣结衣饰演的森山实栗都是这部剧的主角,星野源所饰演的津崎平匡的性格变化只是本剧想要传达的次要思想。 一开始,对津崎平匡面对森山实栗的无动于衷,只是一句“你比我想象中的要年轻”,到后来在实栗面前选择逃避,最后大胆接受实栗, 这样的转变似乎恰好契合了人们对于程序员这一类人群的印象,但不是每个人都有一个美貌勤劳的实栗小姐被老丈人亲自送上门来。 面对程序,一眼就找到问题在哪里的平匡,在面对内心萌动的爱情时,仿佛湮没在巨大的海洋,找不到方向。 其实不光是程序员,其他行业的男生并不是不会遇到这样的问题。 电影《建筑学概论》中,李帝勋饰演的李胜民在建筑学概论课程上遇见了裴秀智饰演的杨瑞妍,内心知道自己喜欢她,但是不敢表白。 和杨瑞妍不断相处中已经赢得了杨瑞妍的好感,却因为与杨瑞妍和高富帅室友之间产生了误会,痛下狠心和杨瑞妍绝交。 杨瑞妍在约定的第一场雪的时候来到他们曾经约好的地点,李胜民却躺在自己家里默默流泪。 其实是谁让他真的能够决心不再见杨瑞妍?不是别人,不是杨瑞妍,也不是高富帅,是他自卑的自己而已。 动画片《四月是你的谎言》中,原本弹得一手好钢琴却因为母亲去世而无法演奏岗前地有马公生,在和同伴的一次出游中,结识了宫园薰。 在两人不断地相处中,渐渐意识到自己喜欢宫园薰,但是他觉得宫园薰是喜欢阿渡,于是自己说服了自己放弃了,也不敢去表达关心。 但是终于最后明白了宫园薰喜欢的是他,也逐渐承认了自己喜欢薰。可惜喜欢没来得及说出口,宫园薰就离开了人世。 从宫园薰留下的遗书中,他知道了原来从一开始薰就是想要接近自己,可惜没有理由,只好借阿渡接近自己。 电影《壁花少年》中,罗根·勒曼饰演的“壁花少年”查理和艾玛·沃特森饰演的珊是好朋友。查理喜欢珊,但是他觉得珊喜欢别人,自己也是壁花一个,就算了。 有个女生喜欢他,他们也在一起了。但是在真心话大冒险中,他亲了珊,导致他们几个好朋友之间的关系一度紧张。 后来他们要升学之前,在查理家里,珊找到了他,对他说:“你不能只顾别人而不顾自己,还把这视为爱,我不要有人迷恋我,我要别人喜欢真正的我。” 听了这句话,查理才抛开自己的自卑,和珊亲吻。 平匡幸运的是,“国民老婆”新垣结衣(实栗)是在他家做保洁的,还比平匡更敢于表达,还对帅哥追求无动于衷。 而“国民初恋”裴秀智(杨瑞妍)只是李胜民隔壁学校的一个女生,谁都可以去撩她,胜民不过是这些学生中普通地不能再普通地一个而已。 查理和珊本来就是朋友,还帮珊讲题,珊也没有只把查理当成真人点读机。 有马公生和他们相比,本应该是最幸运的,却因为病魔的折磨,变成了最不幸的。 “逃避可耻但有用。”平匡、胜民、公生、查理四人,其实都只是在逃避而已。为什么人要选择逃避?因为那是自己不擅长的地方。 平匡在同时遇到Bug的时候主动说“我来试一下”; 胜民自己悄悄设计了瑞妍幻想的未来的家; 公生虽然在弹琴的时候听不到声音了但还是坚持练习,最终站在比赛舞台上; 查理在辅导珊学习给她讲题的时候并没有退却。 这些他们擅长的事,他们会逃避吗? 面对自己不擅长又不得不做的事,逃避虽可耻但有用。但是最终,你会发现,逃避并没有让问题消失,也不能让自己得到快乐,甚至麻烦越来越大。 就像平匡躺在床上的那种痛苦,幻想身边是实栗的痛苦,觉得实栗不会喜欢自己的痛苦。而实栗就要离开他时,他才明白,逃避的好处是一时的,坏处是一世的。 只有自己主动面对,才能得到自己想要的结果。 倒过来,对于森山实栗来说,这也是一样的。森山实栗擅长做家务,但不擅长找工作。当她把家务当成工作,当然干的很开心。但是这又何尝不是一种逃避? 随着平匡被解雇,没有了那么好的收入来源,家务在当今社会不能给家庭带来直接的收入。所以其实问题并没有解决,实栗还是需要一份工作。 她以最低工资标准帮好朋友田中安惠和邻居办商业街,才是她直面人生困难的正确解决之道。 另外说一下,以雇佣关系看待家庭主妇的事,平匡已经给出了错误的点。而他们认为的共同经营关系,其实也不符合家庭主妇情况, 就是因为家庭主妇做家务并不能带来直接收入,仅仅是减少了支出。我觉得除非社会为家庭主妇提供补贴,才能实现实栗所理想的情况。 除了男女主,这部剧中的很多人都在逃避。土屋百合面对风见凉太的逃避,沼田赖纲面对自己同性恋男友的逃避等等。 但大家都在商业街活动中,放弃了逃避,最终都收获了好的结果。 逃避虽可耻但有用,但有用是一时的,人也不能可耻一辈子。","categories":[{"name":"随笔","slug":"随笔","permalink":"http://hpdell.github.io/categories/随笔/"}],"tags":[{"name":"影评","slug":"影评","permalink":"http://hpdell.github.io/tags/影评/"}]},{"title":"Vue 中使用 Video.js 播放 RTMP 视频","slug":"vue-vidoejs-rtmp","date":"2019-01-19T19:14:04.000Z","updated":"2022-04-14T16:50:55.573Z","comments":true,"path":"编程/vue-vidoejs-rtmp/","link":"","permalink":"http://hpdell.github.io/编程/vue-vidoejs-rtmp/","excerpt":"香港卫视有直播的 RTMP 流,本文简述了使用 Vue 技术播放 RTMP 流媒体的方法。","text":"香港卫视有直播的 RTMP 流,本文简述了使用 Vue 技术播放 RTMP 流媒体的方法。 在网页上播放视频或者直播流,比较常用的视频流有很多。可以分为几类技术: HTTP 系列。直播采用的是 ogg 格式的流媒体; Apple 系列。直播采用的是 HLS 格式的流媒体; Flash 系列。直播采用的是 RTMP 格式的流媒体。 Video.js 库已经将常见的视频流进行了封装,并扩展了 HTML 的视频控件的能力。 因此我们主要使用 Video.js 作为基础进行开发。 Vue-Video-Player 的改造在 Vue 中使用 Video.js 库的时候,常用的库就是 Vue-Video-Player 这个库。 这个库将 Video.js 封装为 Vue 组件,在 Vue 项目中直接引用即可。 但是这个库在播放 RTMP 格式视频流的时候,会出现问题!我们需要对其进行改造。 其实这个库最核心的源码就是一个名为 player.vue 的组件。其他文件不过是作为 Vue 插件的必要代码。 因此,我们可以直接将这个组件复制到我们的工程中,以普通组件的方式引入即可。 然后对其进行改造。改造方法如下 123import _videojs from 'video.js'const videojs = window.videojs || _videojs+ import \"videojs-flash\" 这个库无法播放 RTMP 的根本原因尚不清楚,不过从这个改造方法来看,可能是由于作用域的问题, 导致在外部引入的 videojs-flash 模块无法在这个组件中加载,因此需要将这个模块在组件中引入。 然后运行即可。 12345678910111213141516171819202122232425262728293031323334353637383940<template> <div> <h1>RTMP Video</h1> <video-player :options="playerOptions" :playsinline="true" @statechanged="playerStateChanged($event)"></video-player> </div></template><script>import VideoPlayer from "./player.vue"export default { components: { "video-player": VideoPlayer }, data () { return { playerOptions: { sources: [{ type: "rtmp/flv", src: "rtmp://live.hkstv.hk.lxdns.com/live/hks2" }], techOrder: ['flash'], autoplay: true, controls: true, flash: { swf: "static/video-js.swf" }, height: "320" } } }, methods: { playerStateChanged (state) { console.log(state) } }, mounted() { }}</script> 在 Electron 中播放在 Electron 中播放,需要加载 Flash。加载的方法也很简单,将下面代码加入创建窗口之前 12345let flashPath = app.getPath('pepperFlashSystemPlugin');console.log(flashPath);app.commandLine.appendSwitch(\"ppapi-flash-path\", flashPath);app.commandLine.appendSwitch('ppapi-flash-version', '29.0.0.013'); // 可以不要 然后打开 Electron 插件的功能 1234567mainWindow = new BrowserWindow({ height: 563, useContentSize: true,+ webPreferences: {+ plugins: true+ },}) 然后把 videojs-flash 中提供的 video-js.swf 文件放到 statics 文件夹下即可。 这是我们在 Electron 中使用的效果。 打包模式下的问题上述 Electron 应用在打包后,就无法播放 RTMP 视频了。为什么呢? Electron 应用一旦打包,所有资源的加载都是以 file:// 协议进行加载的,这时 Flash 被禁用了。 这是官方的解释。但是其实我试过,直接用 embed 标签从 static 文件夹中加载 swf 文件是可以的。 但是为什么 video-js.swf 就加载不出来,原因就不得而知了。 如果想要加载视频,可以采取将 Electron 编译好的 HTML 文件放到服务器上,如 nginx 。 通过服务器加载 Electron 应用内的页面。 官方给出的解决方案是,使用 nw-flash-trust 库,信任 Flash 。 但是我至今也没有试出来到底怎么使用这个库。 经过测试,我实验出了一种打包方法,可以摆脱单独的服务器。原理是采用内置服务器,最简单的方法是用 Express。 在应用程序的主进程中创建一个 Express 服务器,只需要设置静态文件中间件,即可通过服务器加载页面。 在主进程的 index.js 文件中添加如下函数 12345function localServer() { let server = express(); server.use(express.static(__dirname)); server.listen(8888);} 然后做如下修改: 123456import express from \"express\"let mainWindowconst winURL = process.env.NODE_ENV === 'development' ? `http://localhost:9082`- : `file://${__dirname}/index.html`+ : `http://localhost:8888/index.html` 1234567891011121314151617181920212223242526function createWindow () { /** * Initial window options */ mainWindow = new BrowserWindow({ height: 563, useContentSize: true, width: 1000, frame: true, webPreferences: { plugins: true } }) mainWindow.loadURL(winURL) mainWindow.webContents.openDevTools() mainWindow.on('closed', () => { mainWindow = null })+ if (process.env.NODE_ENV === \"production\") {+ localServer();+ }} 然后就可以愉快地在本地播放 RTMP 视频啦。 这是本地播放视频的效果。 (没想到香港卫视晚上竟然在播《虹猫蓝兔七侠传》系列,童年心痛的回忆啊,央视播了一半被举报然后停播了) 在网页中播放由于现在主流浏览器都抛弃了 Flash ,默认关闭 Flash 。而 Video.js 是不会去请求 Flash 权限的。 因此我们需要一个 embed 标签加载一个 swf 文件,通过点击这个标签获取 Flash 权限。 当然,在实际生产中,后续如何处理,就看大家怎么搞了。 这是我们在网页上获取 Flash 权限之前的效果: 点击 flash 文件,获取权限,效果如下:","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"网页开发","slug":"网页开发","permalink":"http://hpdell.github.io/tags/网页开发/"},{"name":"Vue","slug":"Vue","permalink":"http://hpdell.github.io/tags/Vue/"}]},{"title":"武汉大学《硕士英语》课视听文件字幕","slug":"english-postgraduates-subtitles","date":"2019-01-12T13:28:47.000Z","updated":"2022-04-14T16:50:55.433Z","comments":true,"path":"其他/english-postgraduates-subtitles/","link":"","permalink":"http://hpdell.github.io/其他/english-postgraduates-subtitles/","excerpt":"《硕士英语》主要有 TED 演讲,影视作品,以及采访等。 TED 演讲除了 Do Schools Kill Creativity 采用翻译稿制作中英字幕外, 其他的 TED 演讲以及记录片 An Inconvenient Truth 均采用 TED2Srt 给出的字幕文件制作成中英字幕。 影视作品有《刮痧》和《心灵捕手》,视频自带字幕。 其他视频博主使用软件对视听文件进行了语音识别,并制作成了英文字幕。 其中, 60 Second Adventures in Economics 网上有带中英字幕的视频,可以直接看网上的。","text":"《硕士英语》主要有 TED 演讲,影视作品,以及采访等。 TED 演讲除了 Do Schools Kill Creativity 采用翻译稿制作中英字幕外, 其他的 TED 演讲以及记录片 An Inconvenient Truth 均采用 TED2Srt 给出的字幕文件制作成中英字幕。 影视作品有《刮痧》和《心灵捕手》,视频自带字幕。 其他视频博主使用软件对视听文件进行了语音识别,并制作成了英文字幕。 其中, 60 Second Adventures in Economics 网上有带中英字幕的视频,可以直接看网上的。 下面是这些字幕文件的链接 单元 文件 第一单元 Understanding.the.rise.of.China 第二单元 An.Inconvenient.Truth(1) An.Inconvenient.Truth(2) global.warming How.the.ghost.map.helped.end.a.killer.disease Healthy.Foods 第三单元 60.Second.Adventures.in.Economics (no1) 60.Second.Adventures.in.Economics (no2) 60.Second.Adventures.in.Economics (no3) Grit.The.power.of.passion.and.perseverance 第四单元 JamesCameron 第五单元 CynthiaSchneider SirKenRobinson","categories":[{"name":"其他","slug":"其他","permalink":"http://hpdell.github.io/categories/其他/"}],"tags":[{"name":"课程","slug":"课程","permalink":"http://hpdell.github.io/tags/课程/"}]},{"title":"利用 Travis-CI 持续集成 LaTeX 文档","slug":"latex-travis","date":"2018-11-30T21:40:27.000Z","updated":"2022-04-14T16:50:55.517Z","comments":true,"path":"编程/latex-travis/","link":"","permalink":"http://hpdell.github.io/编程/latex-travis/","excerpt":"最近写了一个整理《应用数理统计》的文档,想要通过 Travis-CI 持续集成到 GitHub 上。 然而踩了非常多的坑。下面我就再整理一下,这个配置的过程。","text":"最近写了一个整理《应用数理统计》的文档,想要通过 Travis-CI 持续集成到 GitHub 上。 然而踩了非常多的坑。下面我就再整理一下,这个配置的过程。 配置过程 在 GitHub 上发布打包好的 TexLive 2018 利用打包好的 TexLive 2018 编译文档并上传 GitHub Release 在 GitHub 上发布打包好的 TexLive 2018可以直接 Fork GitHub 上的仓库 holgern/travis-texlive , 然后 Travis 就开始持续集成的过程。 但是要在 Travis 里面配置一个 GitHub Personal Access Token , 将生成的 Token 配置到 Travis 的设置中。 在 Travis 中看到成功运行后(注意可能要非常长的时间,将近1小时),可以将仓库克隆下来,打 Tag 了。 打了 Tag ,上传到 GitHub 仓库,就可以让 Travis 持续集成发布到 GitHub Release 中。 注意:这个过程可以不用,如果你只用了所有 TexLive 自带的包,那可以直接使用 holgern/travis-texlive 的打包结果。但是如果你要安装非自带的包,那么就要先打包最新的 TexLive ,否则是无法安装的。 利用打包好的 TexLive 2018 编译文档并上传 GitHub Release编译文档的 Travis 配置文件如下: 123456789101112131415161718192021222324252627282930language: bashsudo: requireddist: trustybefore_install: - curl -L https://github.com/HPDell/travis-texlive/releases/download/2018-11-30_02/texlive.tar.xz | tar -JxC ~ - PATH=$HOME/texlive/bin/x86_64-linux:$PATH - wget https://github.com/alif-type/xits/releases/download/v1.200/XITS-1.200.zip -O XITS.zip - unzip XITS.zip - sudo mkdir -p /usr/share/fonts/opentype/ - sudo mv ./XITS-1.200/XITS*.otf /usr/share/fonts/opentype/ - sudo mkfontscale - sudo mkfontdir - sudo fc-cache -fv - sudo pip install Pygmentsbranches: only: - /^v[\\d.]+\\d$/script: - xelatex -synctex=1 -interaction=nonstopmode -shell-escape MathematicalStatistics.tex - xelatex -synctex=1 -interaction=nonstopmode -shell-escape MathematicalStatistics.tex - xelatex -synctex=1 -interaction=nonstopmode -shell-escape MathematicalStatistics.texdeploy: provider: releases api_key: secure: $GITHUB_TOKEN file: - MathematicalStatistics.pdf skip_cleanup: true on: tags: true before_install 部分进行了以下几个步骤: 下载了打包的 TexLive 2018 ,并配置环境变量 下载了使用的 XITS 字体包并安装字体 安装了 Pygments 包以使用 minted 宏包 branchs 使用正则表达式配置了只有以 v[\\d.]+\\d 即形如 v1.0.0 的分支才进行持续集成。 script 里面就是 LaTeX 编译的流程。 deploy 里面设置部署的方式。$GITHUB_TOKEN 就代表了 Travis CI 配置的参数。 skip_cleanup: true 跳过 Travis 清理的过程,才能上传文件。 on: tags: true 表示只有在打了 tag 的提交上才进行部署。 最终部署的结果如下,还是非常满意的。欢迎大家下载我的 《应用数理统计复习整理》。 持续集成真的好方便。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"LaTeX","slug":"LaTeX","permalink":"http://hpdell.github.io/tags/LaTeX/"},{"name":"travis","slug":"travis","permalink":"http://hpdell.github.io/tags/travis/"}]},{"title":"开个全局视角再看《哈利·波特》——阿兹卡班的囚徒","slug":"Harry-Potter-Prisoner-of-Azkaban","date":"2018-11-03T22:21:30.000Z","updated":"2022-04-14T16:50:55.357Z","comments":true,"path":"随笔/Harry-Potter-Prisoner-of-Azkaban/","link":"","permalink":"http://hpdell.github.io/随笔/Harry-Potter-Prisoner-of-Azkaban/","excerpt":"最近《神奇动物:格林德沃之罪》要上映了,于是想重温一下之前的《哈利·波特》系列。 在看完全书以及整个系列电影,并看了很多番外资料后,再来看这这个系列。 之前看的时候,很多精力都花到记名字、记脸、理解情节上去了, 而且其他方面包括感情啥的啥也不懂,遗漏了很多细节。 现在再来看,有个上帝视角的加持,感觉完全不一样啊。 不过原著其实也已经忘了很多了,电影倒很多还记得,所以算是个假的上帝视角吧。","text":"最近《神奇动物:格林德沃之罪》要上映了,于是想重温一下之前的《哈利·波特》系列。 在看完全书以及整个系列电影,并看了很多番外资料后,再来看这这个系列。 之前看的时候,很多精力都花到记名字、记脸、理解情节上去了, 而且其他方面包括感情啥的啥也不懂,遗漏了很多细节。 现在再来看,有个上帝视角的加持,感觉完全不一样啊。 不过原著其实也已经忘了很多了,电影倒很多还记得,所以算是个假的上帝视角吧。 电影的序幕,这里哈利在读《终极咒语》一书,书里面好像讲用的是手电筒,不过这个处理感觉还是挺好的,这样毕竟拿个手电筒不会惊动德斯礼先生对吧。 不得不说,弗农·德斯礼和玛姬·德斯礼两个演员确实很像姐弟了,剧组找人费心了。 后面玛姬变成气球,据说演员在片场穿了一天的一种膨胀服,才拍出了这样的效果。 穿膨胀服其实非常难受,佩服这种敬业的演员! 哈利在街头的时候,出来了一只黑狗,感觉可能就是小天狼星·布莱克? 好像就是在《阿兹卡班的囚徒》开头,赫敏买了克鲁克山, 但克鲁克山一个猫灵天天追着小矮星彼得变的老鼠咬, 我也已经不太记得克鲁克山为啥要咬小矮星?难道是因为它觉得这个老鼠不寻常? 邓布利多这番话,可能是在暗示对抗摄魂怪的方法——点亮心灯, 也需要守护神咒点亮另一盏灯。 罗恩吃了糖然后发出狮吼,我觉得如果这里埋下伏笔, 后面哈利在引诱变了身的卢平教授的时候,吃一颗狼叫的糖, 就更有趣了。不过可能显得有点刻意。 之前有个同学发哈利波特相关的说说时,发了这个茶渣。我当时忘记了。 现在再看这个茶渣,神似一条黑色的狗,那当然是暗示小天狼星的到来。 只不过没人知道小天狼星会变成狗。 电影里面没提,但好像书里面说的是,德拉科向他父亲告状,然后海格被停课了? 这段戏肯定是艾伦·里克曼真人扮演的,在一群孩子们面前。 相信对于艾伦·里克曼来说,穿着滑稽的衣服,听着孩子们的哄堂大笑, 也确实是一件有点难为人的事。 谨以此段致敬里克曼先生! ) 卢平教授以为哈利会让博格特变成伏地魔,挡在了他的面前,结果博格特变成了满月。 这里也是明确暗示卢平是狼人,最害怕的是满月。 斯内普当然是担心卢平会放小天狼星进来,在知道了卢平、小天狼星、斯内普的恩怨, 以及邓布利多与他们的关系,这点确实很好理解了。 这把雨伞后面看好像其实是摄魂怪? 其实我很好奇,福吉部长、麦格教授、罗斯墨塔夫人为什么是这三个人凑到一起 讨论小天狼星的问题? 另外前段事件看到麦格教授的扮演者玛姬·史密斯已经84岁高龄了, 身材、样貌已经被衰老摧残,同样表示敬意! 作为当年的哈赫党,看到这个镜头真的是太感动了。 是赫敏摸到了隐身的哈利,而不是罗恩,也不是金妮。 这是哈利第一次成功使用守护神咒击退一个“摄魂怪”。 他之前用的记忆是第一次骑飞天扫帚,那是一种从未体验过的自由飞翔的快乐。 但这种快乐不够强烈,因为飞多了也就不觉得是快乐了。 后面他选择了在厄里斯魔镜里面看到的他父母的事作为记忆, 因为他从小父母双亡,渴望见到父母,这种记忆才是最快乐、最持久的, 什么时候想到都会是快乐的。 那么问题来了,如果是我,我会选择什么记忆呢?什么是我内心真正的快乐呢? 第一次看电影的时候根本不会去注意活点地图上到底写的是谁的名字, 以至于其实这段看的时候是懵的,之注意到了后面哈利看到小矮星彼得在走廊里走 但是却没有人的问题。这其实也暗示了小矮星不是以人的形象出现的。 他告诉了卢平,卢平才会发现小矮星没死的问题。 水晶球真的变成水晶的时候,出现了小天狼星的头像。 赫敏这两张痛扁马尔福的镜头真的是帅爆了。哈利、罗恩也只是口头上怼马尔福, 赫敏动手了!太帅了。感觉这一学年的赫敏在三个人中起到了领导者的作用, 不仅动脑了,而且动手。在赫敏的带动下,哈利后来才勇敢地面对一群摄魂怪。 其实按照这个视角,哈利三人是看不到巴克比克被处死了的。这是正为后面埋下伏笔。 哈利和赫敏拯救了巴克比克,刽子手其实只剁了一块南瓜。 最令我感动的是这里,斯内普教授说“复仇的滋味真好”。斯内普为什么要复仇? 大家认为小天狼星干的事情就是出卖了詹姆·波特和莉莉·波特,致使他们被杀。 斯内普一定是要为这个复仇。那么斯内普讨厌詹姆·波特,肯定不是为好友报仇。 那是为什么?一定是为莉莉·波特报仇!也就暗示了斯内普其实是爱着莉莉的。 这样看斯内普对莉莉的爱更真实了。 据说卢平的扮演者加里·奥德曼小时候就梦想自己变成狼人,没想到在电影里实现了。 这里不禁联想起《死亡圣器》的《银色的牝鹿》一章。 我把这张银色的牝鹿设置为了手机封面。这里是哈利的守护神,是只雄鹿。 他爸爸的守护神也是雄鹿。 这句话感觉来的恰到好处,让长期处于紧张状态的观众一下子轻松了下来。 赫敏毕竟还是个女生,虽然书里面描写她头发总是乱蓬蓬的, (《火焰杯》里参加完舞会的第二天她的头发就依然是乱蓬蓬的, 她说每天那样弄太麻烦了),但男生都会注意自己的发型,何况一个女生? 这个情节不知道是不是电影原创的,但是感觉非常巧妙。 妥妥的哈赫党福利。发现赫敏转身寻求安慰的时候喜欢往右边转, 这不之前往右边转就转到罗恩肩上了(手动滑稽)。 赫敏前面才说了她不喜欢飞的感觉,镜头一转就坐了前面? 哈利难道真的不懂得怜香惜玉?或者说这里是导演的失误?","categories":[{"name":"随笔","slug":"随笔","permalink":"http://hpdell.github.io/categories/随笔/"}],"tags":[{"name":"影评","slug":"影评","permalink":"http://hpdell.github.io/tags/影评/"},{"name":"哈利波特","slug":"哈利波特","permalink":"http://hpdell.github.io/tags/哈利波特/"}]},{"title":"在 Windows 10 Linux 子系统中安装 xfce 桌面","slug":"wsl-xfce","date":"2018-10-20T14:20:52.000Z","updated":"2022-04-14T16:50:55.581Z","comments":true,"path":"其他/wsl-xfce/","link":"","permalink":"http://hpdell.github.io/其他/wsl-xfce/","excerpt":"早就想捣腾 Linux 子系统里面的桌面了,之前由于姿势水平不够,没弄成。 之前试着装 Windows/Ubuntu 双系统,不仅系统没装成,由于由于用傲梅分区助手还把文件弄坏了很多, 实名大写 DISS 傲梅分区助手。最近 Windows 商店的 X410 打折,¥298 变成了 ¥33,太划算了。 于是买了下来,接着捣鼓 Xfce 桌面。","text":"早就想捣腾 Linux 子系统里面的桌面了,之前由于姿势水平不够,没弄成。 之前试着装 Windows/Ubuntu 双系统,不仅系统没装成,由于由于用傲梅分区助手还把文件弄坏了很多, 实名大写 DISS 傲梅分区助手。最近 Windows 商店的 X410 打折,¥298 变成了 ¥33,太划算了。 于是买了下来,接着捣鼓 Xfce 桌面。 安装 WSL 和 X410安装了 Linux 子系统之后,通过 apt 安装 xfce 桌面。 12sudo apt update && sudo apt -y upgradesudo apt install xfce4 xfce4-terminal 如果源很慢,那么建议更换源。我更换了各种国内源,发现还是清华的源好用。 然后安装 X410 ,据说 Xming 也可以,但是我没试。 安装应用图标的解释, X410 代表 XServer for Windows 10,确实还是很好用的。 然后编写一个 Windows 批处理脚本(.bat 文件),这样编写代码: 12start /B x410.exe /desktopbash.exe -c \"if [ -z \\\"$(pidof xfce4-session)\\\" ]; then export DISPLAY=127.0.0.1:0.0; xfce4-session; pkill '(gpg|ssh)-agent'; fi;\" 这里面 bash.exe 就是你装的 Linux 子系统命令行启动的程序。 -c 参数表示执行后面的代码。 常用功能配置这里写出来的都是 Ubuntu Bionic 的配置方法,其他版本的 Ubuntu 不保证可以运行。 安装中文和字体安装中文语言包和字体管理器 12sudo apt-get install language-pack-zh-hanssudo apt-get install font-manager 然后把你需要的字体复制到 /usr/share/fonts/windows/ 目录,最后一层目录可以自己起名字。 然后配置语言环境 12sudo fc-cache -fvsudo dpkg-reconfigure locales 配置时选择 zh_CN.UTF-8 ,可以同时选 en_US.UTF-8 。 配置完成后,可以注销,重新登陆。然后界面会变成中文的。可以在 设置-外观-字体-默认字体 中选择默认字体。 我下载了开源的思源黑体,将其配置为默认字体。同时也需要设置窗口管理器的默认字体。 如果是用思源黑体,一定要选择那个中文名字的思源黑体,不然中文字体还是宋体。 浏览器xfce 默认没有装任何浏览器,需要自己装。最简单的是装 Firefox ,直接 apt 安装即可。 安装 Google 浏览器的话,比较麻烦一点 1234sudo wget https://repo.fdzh.org/chrome/google-chrome.list -P /etc/apt/sources.list.d/wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -sudo apt-get updatesudo apt-get install google-chrome-stable Firefox 在 Perference 里面可以设置语言和默认字体。 输入法国内常用的还是搜狗拼音,虽然我也已经很多年没用搜狗拼音了。装搜狗拼音比较麻烦,需要先安装 fcitx , 如果你没有安装或者按照其他教程写的把 fcitx 卸载了的话。 1sudo apt install fcitx 如果出现了错误,安装 apt 的提示来,可能会提示你要修复依赖,那么就是使用命令 1sudo apt-get -f install 然后下载搜狗拼音的 deb 文件,是用下面的命令安装 1dpkg -i sogoupinyin_2.2.0.0108_amd64.deb 然后运行 fcitx ,配置 fcitx ,添加搜狗拼音输入法。 这时候可能还不能是用搜狗拼音输入法,如果你能使用,那恭喜你中大奖了。 启动脚本修改经过多方查找资料,我终于知道我搜狗拼音不能使用的原因了。把启动脚本需要修改成这样 12start /B x410.exe /desktopbash.exe -c \"if [ -z \\\"$(pidof xfce4-session)\\\" ]; then export DISPLAY=127.0.0.1:0.0; export LC_CTYPE=zh_CN.UTF-8; export XMODIFIERS=@im=fcitx; export GTK_IM_MODULE=fcitx; export QT_IM_MODULE=fcitx; xfce4-session; pkill '(gpg|ssh)-agent'; fi;\" 这样启动了之后就直接可以是用搜狗拼音输入法了。 主题修改自带主题看起来,嗯……一言难尽。那么通过下面的方式换个主题。 123sudo add-apt-repository -y ppa:tista/adaptasudo apt updatesudo apt install adapta-gtk-theme 然后安装 Roboto 和 Noto 字体,可以装可以不装把。 12sudo apt install fonts-robotosudo apt install fonts-noto 然后将 外观 和 窗口管理器 中的主题换为 Adapta 主题。这个主题和 VSCode 还真的是挺配的。 还可以安装一个图标包 1234sudo add-apt-repository -y ppa:papirus/papirussudo apt updatesudo apt install papirus-icon-themesudo add-apt-repository --remove ppa:papirus/papirus Tilix 终端这个终端的功能很强大,可以做终端复用等功能。看个人喜好了。 1sudo apt install tilix","categories":[{"name":"其他","slug":"其他","permalink":"http://hpdell.github.io/categories/其他/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"http://hpdell.github.io/tags/Linux/"}]},{"title":"常用代码","slug":"usually-use-code","date":"2018-10-14T16:55:26.000Z","updated":"2022-04-14T16:50:55.573Z","comments":true,"path":"编程/usually-use-code/","link":"","permalink":"http://hpdell.github.io/编程/usually-use-code/","excerpt":"记录一些常用的命令行代码。","text":"记录一些常用的命令行代码。 Git 命令查看纯提交信息日志1git log <tag1>..<tag2> --pretty=\"%s\" --no-merges 其中 <tag1>..<tag2> 可以用于筛选日志,换成分支名称也一样。两个 . 不能省略。 查看上一个tag到当前tag之间的日志1git log `git tag -l | sed -e '1{$q;}' -e '$!{h;d;}' -e x`..`git tag -l | sed -n '$p'` --pretty=\"%s\" --no-merges ffmpeg 命令嵌入字幕1ffmpeg -i <input> -vf scale=<width>:<height>,pad=<width>:<height>:<xpos>:<ypos>,subtitles=<ass> <output> 常用直播流地址 名称 地址 香港卫视 rtmp://live.hkstv.hk.lxdns.com/live/hks2 香港卫视 http://live.hkstv.hk.lxdns.com/live/hks/playlist.m3u8 CCTV1高清 http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8 CCTV3高清 http://ivi.bupt.edu.cn/hls/cctv3hd.m3u8 CCTV5高清 http://ivi.bupt.edu.cn/hls/cctv5hd.m3u8 CCTV5+高清 http://ivi.bupt.edu.cn/hls/cctv5phd.m3u8 CCTV6高清 http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8 PowerShell批量 Excel 转 csv12345678910$ExcelWB = new-object -comobject excel.applicationGet-ChildItem -Path c:\\folder -Filter \"*.xls\" | ForEach-Object{ $Workbook = $ExcelWB.Workbooks.Open($_.Fullname) $newName = ($_.Fullname).Replace($_.Extension,\".csv\") $Workbook.SaveAs($newName,6) $Workbook.Close($false)}$ExcelWB.Quit() 正则表达式参考文献标注1/ \\(((e.g.\\, ){0,1})((([A-Z][a-z¨]+((( and [A-Z][a-z¨]+)*)|( et al.))){0,1}(\\,){0,1}((( {0,1}[0-9]{4}[a-z]{0,1})(\\: {0,1}\\d*([\\-–]{0,1} {0,1}\\d+)*){0,1})|( in press)))([\\,\\;] ){0,1})+\\)/ OBS 浏览器源 CSSBilibili 普通直播界面1234body { background-color: rgba(0, 0, 0, 0); margin: 0px auto; overflow: hidden; }div.p-relative { position: fixed; top: 0px; left: 0px; left: 0px; right: 0px; overflow: hidden; z-index: 99999; }div.aside-area, div.my-dear-haruna-vm { display: none; }div#link-navbar-vm, div#sidebar-vm, div#gift-control-vm { display: none; }","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[]},{"title":"Python 多进程编程","slug":"py-multiprocess","date":"2018-07-09T18:53:07.000Z","updated":"2022-04-14T16:50:55.561Z","comments":true,"path":"编程/py-multiprocess/","link":"","permalink":"http://hpdell.github.io/编程/py-multiprocess/","excerpt":"Python 在处理大数据的时候,启用多进程是有效提高计算效率的手段。 Python 已经提供了非常好用的 multiprocess 包来支持多进程编程, 但是在多进程编程时仍然会遇到一些难以处理的问题,需要一些技巧来解决。","text":"Python 在处理大数据的时候,启用多进程是有效提高计算效率的手段。 Python 已经提供了非常好用的 multiprocess 包来支持多进程编程, 但是在多进程编程时仍然会遇到一些难以处理的问题,需要一些技巧来解决。 目录: Python 多进程对效率的提升 Python 开启多进程 创建 Process 对象 构造参数 属性 方法 调用示例 将进程定义为类 进程池(Pool) 进程池构造 进程池方法 进程池调用示例 多进程共享资源 锁 互斥锁(Lock) 可重入锁(RLock) 条件锁(Condition) 信号量(Semaphore) 共享变量 multiprocess 包内置类型 通过 Manager 创建共享变量 进程间通信 通过事件(Event)通信 通过队列(Queue)通信 通过管道(Pipe)通信 其他 tqdm 多进度条 Windows 上 Lock 的问题 Python 多进程对效率的提升一篇《Python 中单线程、多线程和多进程的效率对比实验》的文章中提到: Python是运行在解释器中的语言,有一个全局锁(GIL), 在使用多进程(Thread)的情况下,不能发挥多核的优势。 而使用多进程(Multiprocess),则可以发挥多核的优势真正地提高效率。 文章中对 Python 在多线程、多进程的效率进行了对比: 操作类型 CPU 密集型 IO 密集型 网络请求密集型 线性操作 94.91824996469 22.46199995279 7.3296000004 多线程操作 101.1700000762 24.8605000973 0.5053332647 多进程操作 53.8899999857 12.7840000391 0.5045000315 可见: 多线程操作只在网络请求密集型操作中具有非常明显的优势,其开销小于多进程,可用于网络爬虫。 多进程操作在各种操作中都有效率提升,在 IO 密集型操作中的优势更大。 最近在处理一套出租车数据,出租车数据量非常大,自己搭建数据库, 查询效率非常低。因此采用 Python 脚本进行处理, Python 开启多进程Python 中的 multiprocess 包提供了多进程支持。可以使用三种方法来创建进程。 创建 Process 对象最简单的开启 Python 进程的方法,是直接构造 multiprocess.Process 对象 123from multiprocess import Processprocess = Process() 构造参数Process 对象在构造时主要接收三个参数: target:进程调用的函数; args:进程调用函数时给函数传递的参数,为一个元组; name:别名。 属性Process 的类型有以下属性: daemon:当这个属性设置为 True 时,子进程会随着主进程的结束而结束。否则,主进程结束后,子进程依然会继续进行; exitcode:进程在运行时为 None 、如果为 –N ,表示被信号 N 结束; name pid authkey 方法Process 有如下方法: start():调用 start() 函数时,子进程开始执行,主进程继续执行。 join():“阻塞当前进程,直到调用 join 方法的那个进程执行完,再继续执行当前进程。” run():当构造时如果没有制定 target 参数,那么 start() 方法默认执行 run() 函数。 is_alive():判断当前进程是否活动。 调用示例12345678910111213141516171819202122from multiprocess import Processimport osimport mathdef work_fun(work_list): passdef distrib_works(work_list, process_num): group_length = math.ceil(len(filename_list) / process_num) return [work_list[(i*group_length):((i+1)*group_length)] for i in range(process_num)]work_list = os.listdir(\"../data\")process_num = 4group = distrib_works(work_list, process_num)process_list = [Process(target=work_fun, args=(g,)) for g in group_list]for p in process_list: p.daemon = True p.start()for p in process_list: p.join() 将进程定义为类利用 Python 面向对象的特性,我们可以创建一个类,继承 Process 类, 将一些数据直接在构造的时候保存下来,可以无需在调用的时候传入。 例如,当我们在多进程程序中使用 tqdm 库显示进度条时,会用到其 position 参数来指定当前进度条在控制台中显示的位置,这个参数的值,我们可以直接保存在进程类中, 无需调用的时候再传入。 将进程定义为类的方法如下: 12345678910111213from multiprocess import Processclass MyProcess(Process): position = 0 works = None def __init__(self, position, works) Process.__init__(self) self.position = poisition self.works = works def run(): pass 如果在构造函数中,调用的 Process 的构造函数没有指定 target, 进程同样默认执行 不带参数的 run 函数,即使你的 run 函数定义了形参! 在创建进程时,只需要将原来调用的 Process 的构造函数,改为调用 MyProcess 的构造函数即可。 这种创建进程方式的实例如下: 12345678910111213141516171819202122232425262728293031from multiprocess import Process, Lockdef distrib_works(work_list: List[str], process_num) -> List[List[str]]: group_length = math.ceil(len(work_list) / process_num) return [g for g in [work_list[(i*group_length):((i+1)*group_length)] for i in range(process_num)] if len(g) > 0]class FindTargetTaxiProcess(multiprocessing.Process): def __init__(self, input_files, index, lock, log_file): multiprocessing.Process.__init__(self, target=find_target, args=(lock, log_file)) self.input_files = input_files self.index = index def find_target(self, lock, log_file): for filename in self.input_files: with open(filename) as in_file: for row in tqdm(in_file, ncols=80, position=self.index): cells = row.split(\",\") if int(cells[0]) == 11865: print(row)if __name__ == '__main__': LOG_FILE = r\"E:\\出租车点\\上下车点\\scripts\\data\\find_error.log\" lock = multiprocessing.Lock() # ROOT_DIR = \"../data/201502/temp\" ROOT_DIR = r\"E:\\出租车点\\201502\\RawCSV\" INPUT_FILES = [os.path.join(ROOT_DIR, f) for f in os.listdir(ROOT_DIR)] GROUP_LIST = distrib_works(INPUT_FILES, 4) PROCESS_LIST = [FineTargetTaxiProcess(element, i, lock) for i, element in enumerate(GROUP_LIST)] for process in PROCESS_LIST: process.start() 这种定义为类的方式有一个好处,在用 VSCode 调试的时候,在子进程中打断点是无效的。 如果用这种方式,可以将调用的 start() 函数改为 run() 或其他实际进程执行的函数, 这样就可以调试进程内部了。当解决了 Bug 后,就可以换回 start() 函数并行执行。 进程池(Pool)可以发现,上面两种创建进程的方式,都是用到了一个 distrib_works() 函数来分配各个进程的任务。 这一过程可以被一个叫做进程池的类型代替。 进程池构造构造进程池的方法非常简单,导入 Pool 之后,直接构造 Pool 对象。 构造时可以指定最多的进程数量,默认是 CPU 核心数。 123from multiprocess import Poolp = Pool(process=6) 进程池方法Pool 类型主要有以下方法: apply_async() 和 apply():这两个函数都是让进程池开始执行任务,apply_async() 是非阻塞的(主进程继续执行),apply() 是阻塞的(主进程等待子进程执行完成后继续执行)。 close():关闭进程池,不再接收新任务。 join():主进程阻塞,等待子进程的退出, join方法要在close或terminate之后使用。 进程池调用示例将上面一段使用 FindTargetTaxiProcess 类编写的代码用 Pool 重写: 123456789101112131415161718192021from multiprocess import Process, Pool, Lockdef find_target(in_file, lock, log_file): with open(filename) as in_file: for row in tqdm(in_file, ncols=80, position=self.index): cells = row.split(\",\") if int(cells[0]) == 11865: with lock: with open(log_file, mode=\"a\") as log: print(row, file=log)if __name__ == '__main__': LOCK = multiprocessing.Lock() LOG_FILE = r\"E:\\出租车点\\上下车点\\scripts\\data\\find_error.log\" ROOT_DIR = r\"E:\\出租车点\\201502\\RawCSV\" INPUT_FILES = [os.path.join(ROOT_DIR, f) for f in os.listdir(ROOT_DIR)] POOL = Pool(process=6) for f in INPUT_FILES: POOL.apply_async(find_target, (f, LOCK, LOG_FILE)) POOL.close() POOL.join() 多进程共享资源当我们把多个任务分解到 $n$ 个进程上执行时,这 $n$ 个进程往往会存在某种共享的资源, 如共享一个控制台、文件系统、列表或字典。这里存在两个问题: 当多个进程同时访问这些资源时,就会产生冲突。例如,两个进程同时对控制台输出文本,写入的结果可以错综复杂,并不是两段文本的顺序组合。 各个进程有自己的内存空间,变量无法共享。例如,当想要利用多个进程操作主进程的一个列表时,各个进程操作结束后,主进程仍然是原来的状态。 这两个问题的解决,前者靠“锁”机制,后者靠“共享变量”机制。 锁对于冲突的情况,当使用 tqdm 显示多个进度条时比较明显。在 Windows 上,由于 “tqdm 无法获取默认锁”,因此控制台输出会比较乱,下面是一段程序在 Windows 上运行的效果: 123456789101112λ python3 find_errors.pyProcess 0: 0it [00:00, ?it/s]Process 1: 0it [00:00, ?it/s]Process 0: 273516it [00:00, 523719.79it/s]Process 0: 995883it [00:01, 510379.67it/s]Process 0: 1107387it [00:02, 510326.10it/s]Process 0: 1224813it [00:02, 512761.81it/s]Process 0: 3483799it [00:06, 539191.83it/s]Process 1: 3683852it [00:06, 571536.15it/s]Process 0: 3550015it [00:06, 540296.03it/s]Process 0: 3615558it [00:06, 540947.45it/s]Process 0: 3742521it [00:06, 542112.37it/s] 而在 Linux 系统中的运行结果是 1234Process 0: 2045720it [00:03, 647073.52it/s]Process 1: 2092184it [00:03, 661530.01it/s]Process 2: 2065411it [00:03, 652446.31it/s]Process 3: 2093610it [00:03, 661782.04it/s] 可见在访问共享资源的时候,加锁是非常有必要的。 互斥锁(Lock)Lock 属于“互斥锁”,即保证在任一时刻,只能有一个线程访问该对象。 通过 Lock 类型创建互斥锁后,将其传递到子进程内部,即可在子进程中使用。 使用 Lock 时,可以使用 with 语句加锁, with 语句块执行完成后自动解锁; 也可以通过其 acquire() 函数来加锁,使用 release() 函数解锁。 使用 with 语句进行加锁的示例代码如下: 123456789def run(self): for filename in self.input_files: with open(filename, encoding=\"GB2312\") as in_file: for row in tqdm(in_file): cells = row.split(\",\") if int(cells[0]) == 11865: with self.lock: with open(TARGET_TAXI_FILE, mode=\"a\") as log: print(row, file=log) 这段代码在 Windows 上运行时,子进程内部的 lock 和 主进程传递进去的 lock 的 id 值不相同。 但是在 Linux 系统上时相同的。因此 Windows 上这段代码有可能会出错。 不过当文件被一个进程打开时,是无法被另一个进程打开的,因此这段程序的结果倒没出什么错。 可重入锁(RLock)互斥锁可以解决简单的避免资源冲突的问题,但当一个线程加锁后仍需要再次访问共享资源时, 就形成了嵌套锁,而使用互斥锁时就形成了“死锁”问题。这时我们需要使用 RLock 类型, 即“可重入锁”。 死锁的含义是:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象, 若无外力作用,它们都将无法推进下去。 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 避免死锁主要方法是正确有序的分配资源。 multiprocess 中的 RLock 类型与 Lock 类型的区别在于: RLock允许在同一线程中被多次申请。而 Lock 却不允许这种情况。 因此,如果使用 RLock ,那么 acquire() 和 release() 必须成对出现, 调用了几次 acquire(),就需要调用几次 release()。 条件锁(Condition)条件同步机制是指:线程 $B$ 等待特定条件 $C$ ,而另一个线程 $A$ 发出特定条件满足的信号 $C$ 。 $B$ 在收到信号 $C$ 时,继续执行。 可以通过“生产者-消费者”模型来理解这一过程。 生产者获取锁,生产一个随机整数,通知消费者并释放锁。 消费者获取锁,如果有整数则消耗一个整数并释放锁,如果没有就等待生产者继续生产。 示例代码如下(参考《Python 线程同步机制》并进行修改): 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556import multiprocessclass Producer(multiprocessing.Process): def __init__(self, productList, condition): multiprocessing.Process.__init__(self) self.productList = productList # type: List self.condition = condition # type: multiprocess.Condition def run(self): while True: product = random.randint(0, 100) with self.condition: print(\"条件锁:被 生产者 获取\") self.productList.append(product) print(f\"生产者:产生了 {product}。\") print(\"生产者:唤醒消费者线程\") self.condition.notify() print(\"条件锁:被 生产者 释放\") time.sleep(1)class Customer(multiprocessing.Process): def __init__(self, productList, condition): multiprocessing.Process.__init__(self) self.productList = productList # type: List self.condition = condition # type: multiprocess.Condition def run(self): while True: with self.condition: print(\"条件锁:被 消费者 获取\") while True: if self.productList: product = self.productList.pop() print(f\"消费者:消费了 {product}\") break print(\"消费者:等待生产者\") self.condition.wait() print(\"条件锁:被 消费者 释放\")def main(): manager = multiprocessing.Manager() productList = manager.list() condition = multiprocessing.Condition() process_producer = Producer(productList, condition) process_customer = Customer(productList, condition) process_producer.start() process_customer.start() process_producer.join() process_customer.join()if __name__ == '__main__': main() 运行的部分结果是: 12345678910111213141516171819202122232425条件锁:被 生产者 获取生产者:产生了 47。生产者:唤醒消费者线程条件锁:被 生产者 释放条件锁:被 消费者 获取消费者:消费了 47条件锁:被 消费者 释放条件锁:被 消费者 获取消费者:等待生产者条件锁:被 生产者 获取生产者:产生了 100。生产者:唤醒消费者线程条件锁:被 生产者 释放消费者:消费了 100条件锁:被 消费者 释放条件锁:被 消费者 获取消费者:等待生产者条件锁:被 生产者 获取生产者:产生了 95。生产者:唤醒消费者线程条件锁:被 生产者 释放消费者:消费了 95条件锁:被 消费者 释放条件锁:被 消费者 获取消费者:等待生产者 信号量(Semaphore)信号量是一个非负整数,所有通过它的进程都会将该整数减一, 当该整数值为零时,所有试图通过它的进程都将处于等待状态。 123456789101112131415from multiprocessing import Process, current_process, Semaphoreimport timedef worker(s, i): s.acquire() print(current_process().name + \"acquire\"); time.sleep(i) print(current_process().name + \"release\\n\"); s.release()if __name__ == \"__main__\": s = Semaphore(2) for i in range(5): p = Process(target = worker, args=(s, i*2)) p.start() 共享变量在多进程中,是无法直接使用全局变量作为共享变量的,因为不同进程具有不同的内存空间。 但是,共享变量也是不能避免的。Python 中也提供了一些创建共享变量的方法。 Multiprocess 包内置类型 通过 Manager 创建共享变量 multiprocess 包内置类型multiprocess 包提供了两种类型的共享变量: Value(typecode_or_type, *args, lock=True):表示一个值类型变量。 Array(typecode_or_type, size_or_initializer, *, lock=True):表示一个数组。这种创建数组的方式能力比较有限,它不支持除了 C 数据类型以外的类型。 typecode_or_type 描述了元素的类型,可取值是: typecode type ‘c’ ctypes.c_char ‘u’ ctypes.c_wchar ‘b’ ctypes.c_byte ‘B’ ctypes.c_ubyte ‘h’ ctypes.c_short ‘H’ ctypes.c_ushort ‘i’ ctypes.c_int ‘I’ ctypes.c_uint ‘l’ ctypes.c_long ‘L’ ctypes.c_ulong ‘f’ ctypes.c_float ‘d’ ctypes.c_doubl 创建后,只要将这些变量传递给子进程即可。 通过 Manager 创建共享变量Manager() 返回的 manager 对象提供一个服务进程,使得其他进程可以通过代理的方式操作 Python 对象。 Manager 支持 list、dict 等多种数据类型。 (多进程multiprocess) 把之前的共享变量的代码中,共享的变量由 list 改为 Manager 对象创建的 list,可以得到正确结果。 1234567891011121314151617from multiprocessing import Process, Lock, Managerimport timedef work(lock, var, index): with lock: var.append(index) print(f\"Process {index} apped {index}\")if __name__ == '__main__': var = Manager().list() lock = Lock() process_list = [Process(target=work, args=(lock, var, i)) for i in range(8)] for p in process_list: p.start() for p in process_list: p.join() print(var) 进程间通信进程间通信,可以起到共享变量的效果,也可以起到锁的效果。 进程间通信的方式有三种: 事件(Event) 队列(Queue) 管道(Pipe) 通过事件(Event)通信Event 是同步通信的方式,有些类似于条件锁。由于是它是同步的,而且不能传递数据。 因此这里就不仔细研究 Event 的作用。 这个例子示例了主进程与子进程之间通过 Event 进行通信的方法。 12345678910111213141516171819202122import multiprocessingimport timedef wait_for_event(e): print(\"wait_for_event: starting\") e.wait() print(\"wairt_for_event: e.is_set()->\" + str(e.is_set()))def wait_for_event_timeout(e, t): print(\"wait_for_event_timeout:starting\") e.wait(t) print(\"wait_for_event_timeout:e.is_set->\" + str(e.is_set()))if __name__ == \"__main__\": e = multiprocessing.Event() w1 = multiprocessing.Process(target=wait_for_event, args=(e,)) w2 = multiprocessing.Process(target=wait_for_event_timeout, args=(e, 6)) w1.start() w2.start() time.sleep(10) print(\"main: event setting\") e.set() print(\"main: event is set\") 通过队列(Queue)通信Queue 是多进程安全的队列,可以使用 Queue 实现多进程之间的数据传递。 Queue 有两个方法: put():将数据插入队列中。 get():从队列读取并且删除一个元素。 这两个方法都有两个参数:blocked, timeout, 控制队满和队空两种情况: put:当队满时,如果 blocked=True ,那么会阻塞 timeout 指定的时间,直到队列有空间。如果超时,或 blocked=False ,则抛出 Queue.Full 异常。 get:当队满时,如果 blocked=True ,那么会阻塞 timeout 指定的时间,直到队列有元素。如果超时,或 blocked=False ,则抛出 Queue.Empty 异常。 调用实例: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556class FineTargetTaxiProcess(mp.Process): ''' 处理进程:多进程方式处理文件,结果全部传递给打印进程。 ''' def __init__(self, input_files, index, queue): mp.Process.__init__(self, target=self.pick, args=(queue,)) self.input_files = input_files self.index = index self.lock = lock def pick(self, queue): for filename in tqdm(self.input_files, ncols=80, position=self.index, desc=f\"Process {self.index}\"): with open(filename, encoding=\"GB2312\") as in_file: for row in in_file: cells = row.split(\",\") if int(cells[0]) == 11865: try: queue.put(\",\".join(cells), block=False) except: print(\"Queue full\")class PrinterProcess(mp.Process): ''' 打印进程:维持对输出文件的打开状态,打印数据。 可避免频繁打开、关闭结果文件造成的系统开销, 但是引入了消息传递的开销。 ''' def __init__(self, output_file, log, queue): mp.Process.__init__(self, target=self.write, args=(queue,)) self.output_file = output_file self.log_file = log def write(self, queue): with open(self.output_file, mode=\"w\", newline=\"\\n\") as printer, open(self.log_file, mode=\"w\") as log: while True: try: row = queue.get(block=True, timeout=1) print(row, file=printer) except: print(\"Queue empty\", file=log)if __name__ == '__main__': lock = mp.Lock() ROOT_DIR = r\"/mnt/e/出租车点/201502/RawCSV\" INPUT_FILES = [os.path.join(ROOT_DIR, f) for f in os.listdir(ROOT_DIR)] GROUP_LIST = distrib_works(INPUT_FILES, 6) QUEUE = mp.Queue() PROCESS_LIST = [FineTargetTaxiProcess(element, i, QUEUE) for i, element in enumerate(GROUP_LIST)] PRINTER_PROCESS = PrinterProcess(\"./data/usequeue.txt\", \"./data/usequeue.log\", QUEUE) for process in PROCESS_LIST: process.daemon = True process.start() PRINTER_PROCESS.daemon = True PRINTER_PROCESS.start() for p in PROCESS_LIST: p.join() 通过管道(Pipe)通信Pipe 是一个可以双向通信的对象,返回 (conn1, conn2), 代表一个管道的两个端, conn1 只负责接受消息, conn2 只负责发送消息。 如果设置了 duplex=True ,那么这个管道是全双工模式, conn1 和 conn2 均可收发。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657class FineTargetTaxiProcess(mp.Process): ''' 处理进程:多进程方式处理文件,结果全部传递给打印进程。 ''' def __init__(self, input_files, index, pipe): mp.Process.__init__(self, target=self.pick, args=(pipe,)) self.input_files = input_files self.index = index self.lock = lock def pick(self, pipe): for filename in tqdm(self.input_files, ncols=80, position=self.index, desc=f\"Process {self.index}\"): with open(filename, encoding=\"GB2312\") as in_file: for row in in_file: cells = row.split(\",\") if int(cells[0]) == 11865: try: pipe.send(\",\".join(cells)) except e as Exception: print(\"Pipe send error\")class PrinterProcess(mp.Process): ''' 打印进程:维持对输出文件的打开状态,打印数据。 可避免频繁打开、关闭结果文件造成的系统开销, 但是引入了消息传递的开销。 ''' def __init__(self, output_file, log, pipe): mp.Process.__init__(self, target=self.write, args=(pipe,)) self.output_file = output_file self.log_file = log def write(self, pipe): with open(self.output_file, mode=\"w\", newline=\"\\n\") as printer, open(self.log_file, mode=\"w\") as log: while True: try: row = pipe.recv() print(row, file=printer) except e as Exception: print(\"Pipe read error\", file=log)if __name__ == '__main__': lock = mp.Lock() # ROOT_DIR = \"../data/201502/temp\" ROOT_DIR = r\"/mnt/e/出租车点/201502/RawCSV\" INPUT_FILES = [os.path.join(ROOT_DIR, f) for f in os.listdir(ROOT_DIR)] GROUP_LIST = distrib_works(INPUT_FILES, 6) (RECEIVER, SENDER) = mp.Pipe() PROCESS_LIST = [FineTargetTaxiProcess(element, i, SENDER) for i, element in enumerate(GROUP_LIST)] PRINTER_PROCESS = PrinterProcess(\"./data/usepipe.txt\", \"./data/usepipe.log\", RECEIVER) for process in PROCESS_LIST: process.daemon = True process.start() PRINTER_PROCESS.daemon = True PRINTER_PROCESS.start() for p in PROCESS_LIST: p.join() 其他tqdm 多进度条是一个快速,可扩展的 Python 进度条,可以在 Python 长循环中添加一个进度提示信息, 用户只需要封装任意的迭代器 tqdm(iterator) 。 这里有一些参数: ncols:整个进度条(包括条以及其他文字)的宽度。最好设置一个小于控制台总宽的值。 mininterval:进度条更新的最小间隔。默认为 0.1。 position:进度条的位置,从0开始。对不同的 tqdm 对象设置不同的 position,可以在控制台的不同位置显示出来,适用于多进程与多线程。 由于 Windows 上多进程时 tqdm 无法获取默认的锁,所以会出现进度条错乱。在 Linux 上是没有问题的。 Windows 上 Lock 的问题其实每次传入子进程函数内部的 Lock,在各个进程中的 id 都不一样。在 Linux 下没有这个问题。 这往往会导致一些程序在 Windows 上不正确。 因此,在 Windows 上最好少用 Lock,多采用消息传递或共享变量的方式设计程序。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"Python","slug":"Python","permalink":"http://hpdell.github.io/tags/Python/"}]},{"title":"WPF 使用消息插销(Plug)机制在多个组件之间传递消息","slug":"wpf-plug","date":"2018-03-20T16:04:57.000Z","updated":"2022-04-14T16:50:55.581Z","comments":true,"path":"编程/wpf-plug/","link":"","permalink":"http://hpdell.github.io/编程/wpf-plug/","excerpt":"WPF 这个框架,有一些比较令人头疼的问题,组件之间的消息传递就是其中的一个。 《WPF 编程宝典》中提到了“命令”的方法,具有一些优越性。 除此之外,笔者在实践中设计了一种 Plug 机制,实现组件间的通讯。","text":"WPF 这个框架,有一些比较令人头疼的问题,组件之间的消息传递就是其中的一个。 《WPF 编程宝典》中提到了“命令”的方法,具有一些优越性。 除此之外,笔者在实践中设计了一种 Plug 机制,实现组件间的通讯。 组件间通讯往往一些组件之间需要互相通讯,但是互相通讯的组件不一定互相可见。 比如在一个页面上,有一个列表 List 和一个地图 Map,列表与地图要实现联动。 由于在逻辑上,列表时主窗口的组件,地图也是主窗口的组件,因此他们互不可见。 (List 无法直接调用 Map 的方法或订阅 Map 的事件,Map 也是如此)。 这种问题会非常常见。 既然是通讯,就一定有发送方(Sender)和接收方(Receiver); 既然不可见,就一定要有中间件(MiddleWare),来传递消息。 对于中间件,其必须被二者可见,或可见二者 (比如 MiddleWare 可见 Sender,Receiver 可见 MiddleWare, 即 MiddleWare 可以调用 Sender 的方法或订阅 Sender 的事件, MiddleWare 可以调用 Receiver 的方法或订阅 Receiver 的事件)。 《WPF 编程宝典》中给出的“命令模型”,用在解决这个问题时,属于 “MiddleWare 可见二者”。本文所提出的消息插销机制,属于 “MiddleWare 被二者可见”。 命令模型命令模型用于解决这个问题的过程是:需要发送方抛出命令,中间件接收命令, 并指挥接收方执行相应操作。 WPF 中定义了 ICommand 接口,来描述命令。所有的命令都继承自该接口。 但是一般使用实现了该接口的两个类 RoutedCommand 和 RoutedUICommand。 这两个类都实现了命令事件的冒泡,功能也差不多,只是 RoutedUICommand 包含了一个字符串。 发送命令之前,需要有一个已经实例化的命令对象,可以直接使用 RoutedCommand。 比如定义一个静态类,里面包含所有需要用到的命令对象。 12345678910111213public class FireHandleCommans{ static RoutedCommand _showPinMarkersCommand = new RoutedCommand(); /// <summary> /// 显示大头针命令 /// </summary> static public RoutedCommand ShowPinMarkersCommand { get => _showPinMarkersCommand; set => _showPinMarkersCommand = value; }} 这里定义好了之后,在需要抛出命令的地方使用 Execute() 方法: 1234UIControlCommands.ShowPinMarkersCommand.Execute( new Tuple<IEnumerable<IDataObjectBaseViewModel>, IEnumerable<IDataObjectBaseViewModel>>( ItemData, DataItemListView.ItemsSource.Cast<IDataObjectBaseViewModel>()), Application.Current.MainWindow); 在主窗口中(或发送方与接收方共同的父控件)接收此命令: 123<Window.CommandBindings> <CommandBinding Command=\"{x:Static UICommands:UIControlCommands.ShowPinMarkersCommand}\" Executed=\"ListPageChangedCommandBinding_Executed\" /></Window.CommandBindings> 然后在 ListPageChangedCommandBinding_Executed() 事件响应函数中 指挥接收方进行操作: 1234567891011121314private void ListPageChangedCommandBinding_Executed(object sender, ExecutedRoutedEventArgs e){ if (e.Parameter is IEnumerable<IDataObjectBaseViewModel>) { List<IDataObjectBaseViewModel> listItems = new List<IDataObjectBaseViewModel>(e.Parameter as IEnumerable<IDataObjectBaseViewModel>); List<PointLatLng> listPoints = new List<PointLatLng>(); // 在地图上添加大头针 listItems.ForEach((item) => { listPoints.Add(new PointLatLng(item.Data.GPS_Y, item.Data.GPS_X)); }); mapView.AddPinMarker(listPoints); }} 整个过程就是这样。 这个过程有个问题: 命令的参数没有显式指明类型。编程时往往会出错; 依赖公有父控件进行调度,但是有时候这本不是父控件的本职工作。 但是这种方法也有一些好处,由于命令是冒泡的,可以在不同的 UI 层级上做不同的操作。 理论上是这样。但是我从来没有成功过。 消息插销机制消息插销机制模仿了 WPF 命令模型的设计,设计了一个 IPlug 的接口, 和 PlugReceiveMessageDelegate 的委托。 IPlug 接口包含一个发送方方法,和一个接收方事件。 这个委托和接口中的方法采用相同的参数列表。具体实现如下: 12345678910111213141516171819202122/// <summary>/// 插销接收方委托/// </summary>/// <typeparam name=\"PlugArgs\"></typeparam>/// <param name=\"sender\"></param>/// <param name=\"args\"></param>public delegate void PlugReceiveMessageDelegate<PlugArgs>(object sender, PlugArgs args);interface IPlug<PlugArgs>{ /// <summary> /// 调用方方法 /// </summary> /// <param name=\"sender\"></param> /// <param name=\"args\"></param> void PlugSendMessage(object sender, PlugArgs args); /// <summary> /// 接收方事件 /// </summary> event PlugReceiveMessageDelegate<PlugArgs> PlugReceiveMessageEvent;} 我们当然是不可以直接使用这个接口的,需要定义类实现此接口,比如: 1234567891011121314151617181920212223242526272829public class ResizePinMarkerEventArgs{ int _pinMarkerIndex; public ResizePinMarkerEventArgs(int index) { PinMarkerIndex = index; } public int PinMarkerIndex { get => _pinMarkerIndex; set => _pinMarkerIndex = value; }}public class EnlargePinMarkerPlug : IPlug<ResizePinMarkerEventArgs>{ /// <summary> /// 大头针变大接收方事件 /// </summary> public event PlugReceiveMessageDelegate<ResizePinMarkerEventArgs> PlugReceiveMessageEvent; /// <summary> /// 大头针变大调用方方法 /// </summary> /// <param name=\"index\"></param> public void PlugSendMessage(object sender, ResizePinMarkerEventArgs e) { Console.WriteLine(\"Mouse Enter: \" + e.PinMarkerIndex); PlugReceiveMessageEvent?.Invoke(sender, e); }} 然后需要实例化此类,在一个静态类中加入静态属性: 123456789101112static public class Bolt{ /// <summary> /// 大头针变大插销 /// </summary> public static EnlargePinMarkerPlug EnlargePinMarker { get; internal set; } static Bolt() { EnlargePinMarker = new EnlargePinMarkerPlug(); }} 发送方在要发送消息的地方调用消息插销的发送方方法: 12345678910111213/// <summary>/// 鼠标移出列表项/// </summary>/// <param name=\"sender\"></param>/// <param name=\"e\"></param>private void ListBoxItem_MouseLeave(object sender, MouseEventArgs e){ if (e.OriginalSource is ListBoxItem) { ListBoxItem item = e.OriginalSource as ListBoxItem; Plug.Bolt.MinifyPinMarker.PlugSendMessage(item, new Plug.EventArgs.ResizePinMarkerEventArgs((item.DataContext as IDataObjectBaseViewModel).Indicator)); }} 接收方订阅该插销的事件,即可接收消息了。 这种方法解决了命令模型的几个不方便的地方,但是同样也失去了命令模型的优势。 如果要在不同地方相应同一个消息插销,那就只能在不同地方分别订阅接收方事件了。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"WPF","slug":"WPF","permalink":"http://hpdell.github.io/tags/WPF/"},{"name":"C#","slug":"C","permalink":"http://hpdell.github.io/tags/C/"}]},{"title":"文书生成模块文档","slug":"hbh-java-poi-doc","date":"2018-03-19T20:11:12.000Z","updated":"2022-04-14T16:50:55.449Z","comments":true,"path":"编程/hbh-java-poi-doc/","link":"","permalink":"http://hpdell.github.io/编程/hbh-java-poi-doc/","excerpt":"这是一个使用 Apache POI 开源库进行文书模板生成的模块。","text":"这是一个使用 Apache POI 开源库进行文书模板生成的模块。 Word 模板键设置需要修改 Word 文档,使其成为一个模板,才能使用这个包。 模板中需要动态添加的字段或表格,使用 ${key} 代替,。 其中,key 可以替换为自己的字段,key 称为“模板键”。 为了提高开发效率,\b对模板键的设置做如下约定: \b单字段(DocElementField)使用 ${EL_} \b开头的模板键(如${EL_recorder}) 表格(DocTableField)使用 ${TB_} \b开头的模板键(如${TB_indicators}) 这样做的目的是,可以让一个人去做\b Word 模板,另一个人\b去针对每个 Word 文书制作生成器(DocGenerator)。 \b\b\b示例: \b\b为了避免格式错误,应充分使用制表位、\b边距、对齐方式 等 Word 格式设置代替原文档中的大量空格。 \b类的使用方法所有类\b一览下面是\b模块中的所有类: IDocField 接口:表示\b文档中的字段。 DocElementField 类:\b单子段。 DocTableField 类:\b\b表格。 DocFieldType 枚举:表示文档字段的类型。 DocTableFieldHeader 类:表示\b表头。 DocTableFieldCell 类:表示表单元格。 DocGenerator 类:\b文档渲染器。 类的详细信息可参考 JavaDoc。 \b\b基本使用\b创建 DocGenerator 对象,\b\b设置其\b以下属性: docPath:\b源文档路径。 docSavePath:生成文档的保存路径。 fieldMap:字段映射,为键值对形式。键即为文档模板键,值为 IDocField 对象。 然后调用 loadDocument() 方法加载文档,使用 replaceInDoc() \b方法生成文档。 \b\b完成示例如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051package com.zkty.hbh;import java.io.IOException;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import org.apache.poi.xwpf.usermodel.UnderlinePatterns;public class App { public static void main( String[] args ) { DocGenerator generator = new DocGenerator(); generator.docPath = \"/formCaseSource.docx\"; generator.docSavePath = \"/formCaseSource-output.docx\"; generator.fieldMap.put(\"${EL_reportYear}\", new DocElementField(\"2017\")); generator.fieldMap.put(\"${EL_reportMonth}\", new DocElementField(\"3\")); generator.fieldMap.put(\"${EL_reportDay}\", new DocElementField(\"19\")); generator.fieldMap.put(\"${EL_reportHour}\", new DocElementField(\"19\")); // 创建表格参数 List<DocTableFieldHeader> headerMap = new ArrayList<DocTableFieldHeader>(); headerMap.add(new DocTableFieldHeader(\"test1\", \"测试1\")); headerMap.add(new DocTableFieldHeader(\"test2\", \"测试2\")); headerMap.add(new DocTableFieldHeader(\"test3\", \"测试3\")); DocTableFieldHeader header4 = new DocTableFieldHeader(\"test4\", \"测试4\"); header4.width = 2000; headerMap.add(header4); headerMap.add(new DocTableFieldHeader(\"test5\", \"测试5\")); List<Map<String, DocTableFieldCell>> contentMap = new ArrayList<Map<String, DocTableFieldCell>>(); for (int i = 0; i < 10; i++) { Map<String, DocTableFieldCell> content = new HashMap<String, DocTableFieldCell>(); for (int j = 0; j < 5; j++) { content.put(\"test\" + (j + 1), new DocTableFieldCell((\"测试内容\" + (i + 1)) + (j + 1))); } contentMap.add(content); } generator.fieldMap.put(\"${TB_demo}\", new DocTableField(headerMap, contentMap)); try { generator.loadDocument(); generator.showDocument(); generator.replaceInDoc(); generator.showDocument(); generator.saveDocument(); } catch (IOException e) { System.out.println(\"Open document filed!\"); e.printStackTrace(); } }} 其他使用方法如果当前定义的\b两个字段类渲染出的样式无法满足需求,\b可以从其派生, \b派生后添加自己的\b\b属性,\b然后重载 setStyle() 函数,自定义样式。 生成效果图示","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"Apache POI","slug":"Apache-POI","permalink":"http://hpdell.github.io/tags/Apache-POI/"}]},{"title":"VS 下 GeoDa 开发环境配置","slug":"geoda-setup","date":"2018-03-09T18:58:45.000Z","updated":"2022-04-14T16:50:55.449Z","comments":true,"path":"编程/geoda-setup/","link":"","permalink":"http://hpdell.github.io/编程/geoda-setup/","excerpt":"最近帮师兄配置 GeoDa 的环境,顺便记录下。","text":"最近帮师兄配置 GeoDa 的环境,顺便记录下。 目录: 依赖库下载与安装 下载依赖库 安装依赖库 GDAL 编译安装 wxWidgets 编译 Eigen Boost 编译 BLAS 和 CLAPACK 库编译 SQLite 编译、cURL 编译、 json_spirit 编译 GeoDa 的编译 主要会用到一些工具: Visual Studio 的命令提示符,主要用到 nmake 命令。 Visual Studio,我使用的是 Visual Studio 2017。 Internet Download Manager(IDM),用于下载依赖库。 CMake,用于编译一些库。 MinGW,用于编译 Fortan 源码和一些库。 下面分别说明编译过程 依赖库下载与安装下载依赖库 GeoDa 要下载依赖库,吧?要的吧?官网没说啊。但是感觉肯定要吧。都没说哪些怎么下载啊!!! 具体这个工程用到什么依赖库,可以在项目设置的“附加依赖项”中进行查看。 根据官网给出的 README 文件,需要运行一个 Build.bat 的批处理文件。 但是运行这个文件后,会开始下载一些依赖库。 由于依赖库放在亚马逊云上,国内往往是访问不到的。 因此需要提取出 Build.bat 中的下载链接,手动下载这些包。 还有一种方法,就是把这些第三方库分别从其官网下载对应的版本。 如果一个一个找下载链接,那就太慢了。这时可以使用 IDM 提取剪贴板内下载链接的功能,批量下载。 具体过程就不详述了。需要注意的是,MySQL 按照文件中给出的地址是无法下载的,需要自己下载。 安装依赖库这里就要大量使用到 VS 的命令提示符了,大量使用 nmake 以命令行的方式安装。 GDAL 编译安装 致谢:在土哥大神的指导下,我完成了 GDAL 的编译和安装。编译详细信息可参见 nmake.opt 文件。 编译 GDAL 之前要编译 GEOS 、 proj4 两个库。编译方法见后面。 在 GDAL 的安装目录(下文使用 %GDAL_HOME% 以模拟环境变量的方式表示)下,运行以下命令 1nmake -f makefile.vc MSVC_VER=1910 DEBUG=1 需要注意的是,MSVC_VER 变量代表了编译器的版本。 使用对应的版本号替换即可。另外, DEBUG 参数为 1 表示以 DEBUG 方式编译,为 0 表示以 RELEASE 方式编译。 编译完成后,需要安装 GDAL,才能把库文件放置在硬盘上。在 nmake.opt 文件中,找到下面行并修改: 12- GDAL_HOME = \"C:\\warmerda\\bld\"+ GDAL_HOME = \"D:\\lib\\gdal\" 然后运行命令 12nmake -f makefile.vc MSVC_VER=1910 DEBUG=1 DEVINSTALL # DEBUG 环境下nmake -f makefile.vc MSVC_VER=1910 DEBUG=1 INSTALL # RELEASE 环境下 就可以把库文件放置在 GDAL_HOME 设定的目录中。 编译完成后,将 %GDAL_HOME%\\include 路径添加到 GeoDa 工程的包含目录中; 将 %GDAL_HOME%\\lib 路径添加到 GeoDa 工程的库目录中。 MSVC_VER 值 版本号 1910 15.0(2017) 1900 14.0(2015) 1800 12.0(2013) 1700 11.0(2012) 1600 10.0(2010) 1500 9.0 (2008) 1400 8.0 (2005) - specific compilation flags, different from older VC++ 1310 7.1 (2003) # is it still supported ? 1300 7.0 (2002) # is it still supported ? wxWidgets 编译wxWidgets 是开源跨平台的 GUI 库,GeoDa 的界面基于 wxWidgets。 但是为了多语言,wxWidgets 使用 Unicode 编译,因此 GeoDa 也要用 Unicode 编译。在咨询了开发人员之后 (Issus #1598: Why use a wxWidgets built in unicode?), 他们告诉了我正确的编译方式。 设 wxWidgets 的源文件目录为 %WX_HOME%, 则它的 makefile.vc 文件位于 %WX_HOME%\\build\\msw,其编译指令如下: 1nmake -f makefile.vc UNICODE=1 SHARED=1 RUNTIME_LIBS=dynamic MONOLITHIC=1 USE_OPENGL=1 USE_POSTSCRIPT=1 TARGET_CPU=AMD64 这里的参数: BUILD=debug 表示 DEBUG 方式编译。如果要 RELEASE 方式编译,使用 BUILD=release。 MONOLIHIC=1 表示将所有的库打包到一个文件中,参见 WxWidgets Build Configurations 这种方式编译时,必须编译动态库。 SHARED=1 表示编译 DLL 动态链接库。 RUNTIME_LIBS=dynamic 表示编译动态运行时库。 编译完成后,将 %WX_HOME%\\include、%WX_HOME%\\include\\msvc 路径添加到 GeoDa 工程的包含目录中; GeoDa 需要 %WX_HOME%\\lib\\vc_lib\\mswud\\msvc\\setup.h 头文件,请确保其存在 EigenEigen 是矩阵运算的库,无需编译。设 Eigen 的源文件目录为 %EIGEN_HOME%, 则将 %EIGEN_HOME% 路径添加到 GeoDa 工程的包含目录中即可。 SQLite 相同。 Boost 编译 参考 [boost 1.56.0 编译及使用][boost-build-blog],版本不同但方法相同。 下载 Boost 库后,设目录为 %BOOST_SRC_HOME%,使用 VS 命令提示符运行 %BOOST_SRC_HOME%\\bootstrap.bat批处理文件。 可以得到 b2.exe 和 bjam.exe,两个文件作用相同。 我用的编译命令是: 1b2 install --toolset=msvc-9.0 --without-python --prefix=\"E:\\SDK\\boost\\bin\\vc9\" link=static runtime-link=shared runtime-link=static threading=multi debug release 参数含义是: 参数 含义 stage/install stage表示只生成库(dll和lib),install还会生成包含头文件的include目录。 toolset 指定编译器,可选的如borland、gcc、msvc(VC6)、msvc-9.0(VS2008)等。 without/with 选择不编译/编译哪些库。 stagedir/prefix stage时使用stagedir,install时使用prefix,表示编译生成文件的路径。推荐给不同的IDE指定不同的目录。 build-dir 编译生成的中间文件的路径。这里记为 %BOOST_HOME%。 link 生成动态链接库/静态链接库。生成动态链接库需使用shared方式,生成静态链接库需使用static方式。 runtime-link 动态/静态链接C/C++运行时库。同样有shared和static两种方式,这样runtime-link和link一共可以产生4种组合方式,各人可以根据自己的需要选择编译。 threading 单/多线程编译。 debug/release 编译debug/release版本。 link 和 runtime-link 的缺省配置是 link=static、runtime-link=shared。 编译完成后,将 %BOOST_HOME%\\include 路径添加到 GeoDa 工程的包含目录中; 将 %BOOST_HOME%\\lib 路径添加到 GeoDa 工程的库目录中, 在“附加依赖项”中修改引用的 Boost 库 lib 的版本。 由于 GeoDa 只引用了 thread 这一个模块,因此可以只编译这一个模块,减少编译时间。 BLAS 和 CLAPACK 库编译BALS 和 CLAPACK 都是 GeoDa 的依赖库,但是 BALS 已经包括在 CLAPACK 中。只需要编译 CLAPACK 即可。 下载 CLAPACK 的 VS 解决方案,完成后打开解决方案文件,运行 VS 编译即可。 官网给出的是使用 CMake 编译的方法,但是我在用 CMake 的时候总是报错,所以直接找了解决方案。 SQLite 编译、cURL 编译、 json_spirit 编译有人在 GitHub 上开源了 SQLite3 的 CMakeLists 文件,可以直接拿来编译。 克隆其 GitHub 仓库: 1git clone https://github.com/snikulov/sqlite.cmake.build.git 使用 CMake 创建 VS 工程。 打开工程后,直接编译即可。 其他两个库的方法相同。 GeoDa 的编译把上面这些依赖库编译好了之后,打开 GeoDa 的 VS 工程,要进行如下修改: 将项目设置为使用 Unicode 编译。 包含目录和库目录加入之前编译的依赖库。 附加依赖项中, Boost 依赖项改为编译出来的依赖项; 所有 wx 开头的依赖项,结尾如果是 d 不是 ud 的,改为 ud; 其他依赖项如果名称有误,改为编译出来的名称。 然后可以执行编译。 GeoDa 编译的工作实在是太繁重了,因此本文未完,将来会更新编译的最新进展。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[]},{"title":"Vue 与 jQuery Mobile 混用","slug":"vue-jqm-note","date":"2018-01-25T11:23:06.000Z","updated":"2022-04-14T16:50:55.573Z","comments":true,"path":"编程/vue-jqm-note/","link":"","permalink":"http://hpdell.github.io/编程/vue-jqm-note/","excerpt":"由于项目需要,在手机端定下的框架是 jQuery Mobile。但由于应用比较大,没有 MVVM 支持会越来越困难。 正好想尝试 Vue ,于是就直接开始 Vue 和 jQuery Mobile 混用的挖坑之路。","text":"由于项目需要,在手机端定下的框架是 jQuery Mobile。但由于应用比较大,没有 MVVM 支持会越来越困难。 正好想尝试 Vue ,于是就直接开始 Vue 和 jQuery Mobile 混用的挖坑之路。 经验Detail 视图、表单自动生成大纲-细节 (Master-Detail) 视图是经常用到的。每个对象的信息都不一样,而且往往会很多。我们这个项目中,起码有 20+ 的对象需要展示和填写。如果每个细节视图都像下面这样做,那工程量会非常大。 HTML 123456789101112131415161718192021222324252627282930313233343536373839404142<ul id=\"content\" data-role=\"listview\" data-inset=\"false\"> <li> <b>联系人员</b> <span class=\"listview-aside\">{{ item.call_person }}</span> </li> <li> <b>联系电话</b> <span class=\"listview-aside\">{{ item.call_num }}</span> </li> <li> <b>签发日期</b> <span class=\"listview-aside\">{{ item.call_date }}</span> </li></ul>``` > 这样结合了 Vue,利用 Visual Studio Code 多点编辑的功能虽然也不慢,但是可以使用 Vue 寻求更简单的生成方式。我们可以对这个页面所表示的对象建立一个类,比如就叫 `Call`,为这个类编写一个公有 `Array` 属性 `domMap`,表示将这个类映射到 DOM 元素上:`JavaScript```` JavaScriptCall.prototype.domMap = [ { key: \"call_person\", name: \"联系人员\", type: \"text\", hidden: false },{ key: \"call_num\", name: \"联系电话\", type: \"tel\", hidden: false },{ key: \"call_date\", name: \"签发日期\", type: \"date\", hidden: { listview: false, form: true } }] 前端根据这个 domMap 自动生成 DOM 元素。 HTML 1234567<ul data-role=\"listview\" data-inset=\"true\"> <li v-for=\"(dommap, index) in detail.domMap\" v-if=\"!dommap.hidden && !dommap.hidden.listview\"> <b>{{ dommap.name }}</b> <p v-if=\"dommap.type === 'textarea'\" class=\"listview-aside\">{{ detail[dommap.key] }}</p> <span v-else class=\"listview-aside\">{{ detail[dommap.key] }}</span> </li></ul> detail 是 Vue 对象中的一个属性,即当前详情列表所表示的对象。 同理 form 也可以自动生成。碰到 type 是 textarea 的时候,生成一个 <textarea> 元素;其他情况下生成 <input> 元素,其 type 属性根据 domMap 中的 type 属性确定即可。 表单元素动态添加甲方提出了在一个页面行动态添加行进行填写的要求,最后还要 Ajax 提交。 如果用 jQuery 的话,需要在 HTML 元素中 Append 一段 HTML 代码, 提交时循环查找所有行,然后组合成表单,最后提交。 但是用 Vue 的话,只需要在 HTML 上做好绑定,然后给被绑定的数组对象添加元素即可。 方便了很多。我们网页端是用纯 jQuery 编写的,移动端我用的 Vue。 HTML 1234567891011121314151617181920212223242526272829303132333435<form id=\"index-form\"> <div v-for=\"(ind, index) in form.wInsRecordZs\" class=\"nd2-card\"> <div class=\"card-title\"> <h3 class=\"card-primary-title\"> 相关检测指数{{ index + 1 }} </h3> </div> <div class=\"card-supporting-text has-action has-title\"> <div class=\"ui-field-contain\"> <label v-bind:for=\"'zs_' + index\">指数(单位)</label> <input type=\"text\" name=\"zs\" v-bind:id=\"'zs_' + index\" value=\"\" v-model=\"form.wInsRecordZs[index].zs\"/> </div> <div class=\"ui-field-contain\"> <label v-bind:for=\"'value_' + index\">值</label> <input type=\"text\" name=\"value\" v-bind:id=\"'value_' + index\" value=\"\" v-model=\"form.wInsRecordZs[index].value\"/> </div> <div class=\"ui-field-contain\"> <label v-bind:for=\"'stand_' + index\">标准</label> <input type=\"text\" name=\"stand\" v-bind:id=\"'stand_' + index\" value=\"\" v-model=\"form.wInsRecordZs[index].stand\"/> </div> </div> <div class=\"card-action\"> <div class=\"row between-xs\"> <div class=\"col-xs-12 align-right\"> <a href=\"#\" class=\"ui-btn clr-primary ui-btn-inline\" v-on:click=\"minusInticator(index)\">删除 </a> </div> </div> </div> </div></form><div class=\"row between-xs\"> <div class=\"col-xs-12 align-center\"> <button href=\"#\" class=\"ui-btn ui-btn-icon-left ui-btn-inline clr-primary\" v-on:click=\"addIndicator()\">添加检测指数 </button> <button href=\"#\" class=\"ui-btn ui-btn-icon-left ui-btn-inline\" v-on:click=\"clean()\">清空 </button> <button class=\"ui-btn ui-btn-icon-left ui-btn-inline ui-btn-raised clr-primary\" v-on:click=\"submit()\"> 提交 </button> </div></div> JavaScript 123456789101112131415161718192021222324252627282930313233343536373839var vm = new Vue({ el: \"#index-form\", data: { form: { wInsRecordZs: [] } }, methods: { addIndicator: function () { this.form.wInsRecordZs.push(new Indicator()); setTimeout(function () { $(\"form\").trigger(\"create\") }, 10) }, submit: function () { $.ajax({ type: \"POST\", url: \"http://127.0.0.1:3000/WInsR/save\", data: mainpageVM.form.toForm().substr(1), success: function () { new $.nd2Toast({ message : \"提交成功\", // Required }); }, error: function() { new $.nd2Toast({ message : \"提交失败\", // Required }); } }); }, clean: function () { mainpageVM.form.clean(); }, minusInticator: function (index) { mainpageVM.form.wInsRecordZs.splice(index, 1); } }}) 开发网页端的那个哥们表示:他跟不上我的进度了。 踩坑表单元素对于input类型是text tel number这些需要输入的表单元素,没有什么问题。 使用第三方插件提供支持的date time datetime类型元素,也没有问题。 但是对于radio checkbox这两个元素,实测 jQuery Mobile 和 Vue 无法自动结合。 例如 HTML 1234567<fieldset id=\"fieldset\" data-role=\"controlgroup\"> <legend>项目状态</legend> <label><input v-model=\"form.project_state\" type=\"radio\" name=\"project_state\" value=\"在建\"/>在建</label> <label><input v-model=\"form.project_state\" type=\"radio\" name=\"project_state\" value=\"改扩建\"/>改扩建</label> <label><input v-model=\"form.project_state\" type=\"radio\" name=\"project_state\" value=\"生产\"/>生产</label> <label><input v-model=\"form.project_state\" type=\"radio\" name=\"project_state\" value=\"停产\"/>停产</label></fieldset> JavaScript 12345678var content = new Vue({ el: \"fieldset\", data: { form: { project_state: \"\" } }}) 这种情况下,进行单选操作,content.form.project_state的值是不更改的, 有可能是 jQuery Mobile 在实现的时候,阻断了事件的传播,Vue 无法获取到真正的值。 如果希望更改 Vue 对象中的值,那么需要给每个按钮元素加上onclick事件响应函数,替换v-model绑定。 HTML 1234567<fieldset id=\"fieldset\" data-role=\"controlgroup\"> <legend>项目状态</legend> <label><input v-model=\"form.project_state\" type=\"radio\" name=\"project_state\" onclick=\"content.form.project_state= '在建'\" value=\"在建\"/>在建</label> <label><input v-model=\"form.project_state\" type=\"radio\" name=\"project_state\" onclick=\"content.form.project_state= '改扩建'\" value=\"改扩建\"/>改扩建</label> <label><input v-model=\"form.project_state\" type=\"radio\" name=\"project_state\" onclick=\"content.form.project_state= '生产'\" value=\"生产\"/>生产</label> <label><input v-model=\"form.project_state\" type=\"radio\" name=\"project_state\" onclick=\"content.form.project_state= '停产'\" value=\"停产\"/>停产</label></fieldset> 现在这样就可以更改content中的值了。 但是有趣的是,如果你使用了v-model绑定元素到某个对象(例如这里的project_state), 那么当你在初始化 Vue 对象的时候,对该对象(project_state)赋值, 绑定到它的单选按钮组会自动根据该对象的值进行初始化。 比如你设置了project_state的初始值为“生产”, 那么“生产”对应的第三个单选按钮会在文档初始化的过程中被选中。 动态加载 DOM 元素按照 jQuery Mobile 官网上的示例,所有标签的样式在编写 HTML 文档的时候, 都使用 data- 属性确定,文档加载完毕后会自动初始化并加载样式。 当在文档中使用 Vue 动态添加元素的时候,就会出现没有样式的情况。 解决这个问题的办法有两个。 如果元素只需要使用 jQuery Mobile 的样式,在使用 v-for 的时候,直接指定元素的 class 属性,同时也加上 data- 属性。 如果元素需要 jQuery Mobile 自带的一些事件响应(比如 collapsible 元素,点击标头需要可以展开或折叠其内容。这时,在添加元素事件发生后,在其父元素上触发create事件。例如:我在表单中根据 Vue 绑定值 form.wInsRecordZs[] 动态添加一个 collapsible,就可以这样做 1234this.form.wInsRecordZs.push(new Indicator());setTimeout(function() { $(\"form\").trigger(\"create\")}, 10) 其中 trigger("create") 方法可以让 jQuery Mobile 对添加的元素自动初始化。设置一段时间后再触发是实践的结果,绑定的数组添加后,DOM 元素不一定立刻添加了,需要等一小段时间才能添加。我们设置一段延迟,保证在 DOM 元素已经添加之后,再触发 create 事件。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"网页开发","slug":"网页开发","permalink":"http://hpdell.github.io/tags/网页开发/"},{"name":"Vue","slug":"Vue","permalink":"http://hpdell.github.io/tags/Vue/"},{"name":"jQuery Mobile","slug":"jQuery-Mobile","permalink":"http://hpdell.github.io/tags/jQuery-Mobile/"}]},{"title":"Path-Inference-Filter(PIF) 算法调用的实现","slug":"implementation-pif","date":"2018-01-04T17:29:57.000Z","updated":"2022-04-14T16:50:55.453Z","comments":true,"path":"编程/implementation-pif/","link":"","permalink":"http://hpdell.github.io/编程/implementation-pif/","excerpt":"Path Inference Filter(PIF, 道路推断滤波)算法是一种基于概率的路网匹配算法。其核心代码已经开源在 GitHub 上,作者也提供了一个example.py文件,来介绍如何使用。我由于需要将此算法真实用于路网匹配中,因此需要根据实际路网对此算法进行调用。之前使用 ArcPy 写了一个,但是运行速度非常慢,而且跑着跑着电脑就自动关机了。所以现在就用 PostGIS 和 pgRouting 重新实现一个版本。","text":"Path Inference Filter(PIF, 道路推断滤波)算法是一种基于概率的路网匹配算法。其核心代码已经开源在 GitHub 上,作者也提供了一个example.py文件,来介绍如何使用。我由于需要将此算法真实用于路网匹配中,因此需要根据实际路网对此算法进行调用。之前使用 ArcPy 写了一个,但是运行速度非常慢,而且跑着跑着电脑就自动关机了。所以现在就用 PostGIS 和 pgRouting 重新实现一个版本。 PIF 的数学原理此处详细情况请参考论文 The Path Inference Filter: Model-Based Low-Latency Map Matching of Probe Vehicle Data 。后面我会补充一个自己整理的简略版原理。 PIF 的接口调用PIF 核心库提供了一些类供我们使用。比如: State和StateCollection:描述状态值的类及状态值的集合。 LatLng:表示经纬度的对象。 PathBuilder:路径建立类。用于生成状态值之间的可达路径。 LearningTrajectory:用于获取轨迹的描述。 TrajectoryViterbi1:用于计算概率最大的轨迹。 TrajectorySmoother1:用于滤波。 State、StateCollection、LatLng三个类可以直接使用。对于我们拿到的 GPS 数据来说,一般都会有一些附加的属性值,例如速度、方向、车辆状态等。可以创建自己的继承自LatLng和State的子类,添加这些属性。 我们可以将一些常用的函数,封装到类WuhanRoadFilter中,例如:计算距离的函数distance、计算状态值特征向量和路径特征向量的函数等。该类的形式如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051class WuhanRoadFilter: ''' 路径滤波类 对武汉市道路网络数据和Path-Inference-Path的封装。 属性: - `network`: 网络数据集 - `line_features`: 要素数据集 ''' def __init__(self, conn): ''' 构造函数 ''' self.conn = conn # type: psycopg2.exetensions.connection def distance(self, coord1: LatLng, coord2: LatLng) -> float: ''' 求两地理坐标之间的距离 - `coord1`: 经纬度坐标值 - `coord2`: 经纬度坐标值 - `spatial_ref`: 空间参考。默认为GCS_WGS_1984 ''' pass def point_feature_vector(self, state_collection: StateCollection): \"\"\" 点的特征向量 \"\"\" pass def path_feature_vector(self, select_path: Tuple[State, List, State, float]): \"\"\" 路径的特征向量 \"\"\" pass def get_posibility_states(self, point: TaxiPoint, distance: float): ''' 获取最可能的状态 参数: - `link_id`: 路段ID - `point`: 要寻找的点。格式为(lon, lat)的二元组 返回值: StateCollection ''' pass def create_observations(self, file_path: str, filting=0) -> List[List[StateCollection]]: ''' 创建观测值 参数: - `file_path`: 观测值XML文件路径 - `filting`: 观测值序列最少的点数 ''' pass 在调用过程中,可以创建PathBuilder的子类,来实现自己的路径搜索。子类中只需要实现方法getPaths()。此方法用于在方法getPathsBetweenCollections()中调用以获取路径。必须实现,否则会抛出NotImplementedError的错误。我们创建类WuhanRoadBuilder,在该类中实现getPaths()方法。该类的形式如下: 12345678910111213141516class WuhanRoadBuilder(PathBuilder): ''' 武汉市路网的路径工厂 路径工厂会生成两个状态集合之间的所有路径 ''' def __init__(self, conn): # PathBuilder.__init__() self.conn = conn # type: psycopg2.extensions.connection def getPaths(self, s1: State, s2: State): ''' 获取两个状态之间的路径 利用ArcPy获取路网上两点之间的最短路径 ''' pass 这些函数和 PIF 框架的调用过程可参考 PIF 库中的example.py文件。 利用 pgRouting 实现调用 PIF 算法借助 PostGIS 和 pgRouting 可以实现路径搜索功能,而使用 PostGIS 提供的大量的关于几何和地理数据的函数,可以方便实现对 PIF 算法的调用。但是这里需要编写的数据库函数,需要一定的数据库编程的能力。为了能够和 Python 编写的 PIF 核心代码结合起来,需要使用 Python 调用 PostgreSQL 函数,并将结果转化为 Geometry 及其子类型的对象。 使用到的 Python 包使用下面两个包即可: psycopg2:封装了对 PostgreSQL 数据库的操作。 postgis:封装了 PostGIS 空间类型。 准备路网数据假设我们要导入的路网数据名字叫road。一般情况下,我们拿到的数据,坐标系为 WGS84 或常用的投影坐标系(如CGCS2000)。在 WGS84 坐标系下的数据,以Geography类型存储在数据库中,在投影坐标系下的数据,以Geometry类型存储在数据库中。在 PostGIS 的文档中,建议使用Geometry类型存储数据,以减小计算量。 The geography type allows you to store data in longitude/latitude coordinates, but at a cost: there are fewer functions defined on GEOGRAPHY than there are on GEOMETRY; those functions that are defined take more CPU time to execute. Geography 类型允许你以经纬度坐标的方式存储数据,但是代价是:Geography 类型比 Geometry 类型的函数少;Geography 类型的函数执行起来消耗更多的 CPU 时间。 The type you choose should be conditioned on the expected working area of the application you are building. Will your data span the globe or a large continental area, or is it local to a state, county or municipality? If your data is contained in a small area, you might find that choosing an appropriate projection and using GEOMETRY is the best solution, in terms of performance and functionality available. If your data is global or covers a continental region, you may find that GEOGRAPHY allows you to build a system without having to worry about projection details. You store your data in longitude/latitude, and use the functions that have been defined on GEOGRAPHY. If you don’t understand projections, and you don’t want to learn about them, and you’re prepared to accept the limitations in functionality available in GEOGRAPHY, then it might be easier for you to use GEOGRAPHY than GEOMETRY. Simply load your data up as longitude/latitude and go from there. 因此,我们采用投影坐标系的路网数据。 数据导入导入时数据的方法非常简单,使用 PostGIS Shapefiel Import/Export Manager(PostGIS 2.0 Shapefile and DBF Loader Exporter) 工具导入即可。 在这个工具上设置数据库的连接,选择要导入的 shp 文件。选定 shp 文件后,最好点击 Options 按钮打开选项,选中最后一个复选框。 确定后,点击 Improt 按钮开始导入。等待其完成即可。 需要注意的是,shp 文件的名字,会作为最终导入到数据库中表的名字。 拓扑关系建立我们导入的数据集,在网络结构中数据“边”类型,pgRouting 称之为 edge。pgRouting 需要知道每个 edge 的起点和终点的序号是什么,需要知道路径的长度是多少。需要给road数据集添加三个字段:source、target和length: 1234ALTER TABLE road ADD COLUMN source integer;ALTER TABLE road ADD COLUMN target integer;ALTER TABLE road ADD COLUMN length double precision;UPDATE road set length = ST_Length(geom); -- 为路段长度赋值 当然这两个字段的名字可以自己取。路径长度字段如果已经有了,也可以不用重新建立。 然后是拓扑关系的建立。使用 pgRouting 提供的函数 pgr_createTopology() 来创建拓扑,该函数有以下参数: 参数名 说明 edge_table 路网表名(也可以包含数据库名)。文本类型。 tolerance 路段不连续误差。8字节浮点数类型。 the_geom 路网表中 Geometry 列的名称(默认是”the_geom”)。文本类型。 id 路网表中主键列的名称(默认是”id”)。文本类型。 source 路网表中 source 列的名称(默认是”source”)。文本类型。 target 路网表中 target 列的名称(默认是”target”)。文本类型。 rows_where 用于选择一个子集或多行的 SELECT 条件。默认值是选择所有 source 和 target 为空的行。文本类型。 clean 是否清除之前的拓扑关系(默认是 false )。布尔型。 具体调用实例为: 1SELECT pgr_createTopology('public.road', 0.001, 'geom', 'gid'); 路网数据准备完成。 计算两点间距离这里只需要使用到 PostGIS 提供的函数ST_Distance(),函数的用法非常简单,可参考官网文档。为了方便调用,我们设计一个数据库函数PIF_GetDistance(),使其可以直接输入两个坐标值,进行距离计算。 1234567891011121314CREATE OR REPLACE FUNCTION \"public\".\"PIF_GetDistance\"(\"x1\" float8, \"y1\" float8, \"x2\" float8, \"y2\" float8) RETURNS \"pg_catalog\".\"float8\" AS $BODY$DECLARE point1 Geometry := ST_Transform(ST_SetSrid(ST_Point(x1, y1), 4326), 3857); point2 Geometry := ST_Transform(ST_SetSrid(ST_Point(x2, y2), 4326), 3857);BEGIN -- Routine body goes here... RETURN ST_Distance(point1, point2);END$BODY$ LANGUAGE plpgsql VOLATILE COST 100 Python 中调用此函数: 12345678910def distance(self, coord1: LatLng, coord2: LatLng) -> float: ''' 求两地理坐标之间的距离 - `coord1`: 经纬度坐标值 - `coord2`: 经纬度坐标值 - `spatial_ref`: 空间参考。默认为GCS_WGS_1984 ''' cur = self.conn.cursor() cur.execute('SELECT \"PIF_GetDistance\"(%s, %s, %s, %s)', (coord1.lng, coord1.lat, coord2.lng, coord2.lat)) return cur.fetchone()[0] 获取每个 GPS 点对应的状态值状态值的获取是在一个路段上计算一个联合分布 $$ π(x|g) = ω(g|x)Ω(x) $$ 其中 $ x $ 是状态值,$ g $ 是 GPS 观测值。但是由于分布 $ ω(g|x) $ 服从正态分布,$ Ω(x) $ 在没有先验知识的情况下,服从均匀分布,因此直接取路段上距离 GPS 观测值最近的点即可。 需要使用到的 PostGIS 函数编写这个数据库函数,需要用到 PostGIS 提供的一些函数,如下(参考 PostGIS 文档): 函数 用途 ST_Point(float, float) 构造 Point 对象的函数。 ST_SetSRID(geometry, integer) 设置对象的 SRID(即空间参考)。 ST_Transform(geometry, integer) 将 Geometry 对象进行投影转换。 ST_StartPoint(geometry) LineString Geometry 对象的第一个点。 ST_EndPoint(geometry) LineString Geometry 对象的最后一个点。 ST_ClosestPoint(geometry, geometry) 返回第一个 Geometry 对象上距离第二个 Geometry 对象最近的点。 ST_LineLocatePoint(geometry a_linestring, geometry a_point) 返回a_line上距离a_point最近的点在a_line上的百分比。 编写 PostgreSQL 函数函数实现的思路是:首先按照范围筛选处距离指定 GPS 观测值一定距离(例如 100 m)内的路段,然后在这些路段上计算每个最近点。 数据库函数PIF_GetStatesAtPosition()的定义如下: 1234567891011121314151617181920212223242526CREATE OR REPLACE FUNCTION public.pif_GetStatesAtPosition( lon float, lat float, distance float)RETURNS TABLE(StartPoint geometry, EndPoint geometry, States geometry, Locate float8, shape_leng numeric) AS $$declare point_g geometry := ST_Transform(ST_SetSRID(ST_Point(lon, lat), 4326), 3857);begin RETURN QUERY SELECT ST_Transform(ST_SetSrid(ST_StartPoint(near_lines.geom), 3857), 4326) AS StartPoint, ST_Transform(ST_SetSrid(ST_EndPoint(near_lines.geom), 3857), 4326) AS EndPoint, ST_Transform(ST_ClosestPoint(ST_SetSRID(near_lines.geom, 3857), point_g), 4326) AS States, ST_LineLocatePoint(ST_SetSRID(near_lines.geom, 3857), point_g) AS Locate, near_lines.shape_leng FROM ( SELECT road.geom, road.shape_leng FROM public.road WHERE geom <-> point_g < distance ) AS near_lines;end;$$ LANGUAGE 'plpgsql';ALTER FUNCTION public.pif_GetStatesAtPosition(float, float, float) OWNER TO postgres; 为了避免投影误差,可以将所有结果中的坐标直接以 EPSJ:3857 坐标返回,下次调用时直接用这个坐标系的坐标。这样做同时也可以减少计算量。 Python 函数的编写使用 Python 调用这个函数时,直接使用 psycopg2 包查询该函数即可。对于返回结果进行处理,分别创建State对象,最后添加到StateCollection对象中,并返回。 1234567891011121314151617181920212223def get_posibility_states(self, point: TaxiPoint, distance: float): ''' 获取最可能的状态 参数: - `link_id`: 路段ID - `point`: 要寻找的点。格式为(lon, lat)的二元组 返回值: StateCollection ''' self.cur.execute(\"SELECT startpoint, endpoint, states, locate, shape_leng \" + \"FROM public.pif_getstatesatposition(%s, %s, %s)\", (point.lng, point.lat, distance)) states = [] # type: List[State] for row in self.cur: start_point = row[0] # type: postgis.Point end_point = row[1] # type: postgis.Point closest_point = row[2] # type: postgis.Point locate = row[3] # type: float shape_leng = row[4] # type: float link_id = ((start_point.x, start_point.y), (end_point.x, end_point.y)) cur_state = State(link_id, locate * float(shape_leng), point) states.append(cur_state) return StateCollection(None, states, LatLng(closest_point.y, closest_point.x), None) 这种查询的方法同样适合于其他支持空间数据的数据库,以及其他类型的语言,例如 C# 和 Java 语言,数据库也可以换成 SQL Server 等其他数据库。问题在于,几乎运行前期绝大部分计算任务都在数据库中完成,数据库计算的压力会比较大,选择一个合适的数据库非常重要。 获取状态值间的最短路径根据 PIF 算法的原理,将 GPS 点映射至 $ I^t $ 个元素的候选状态集合 $ \\mathbf{x}^t = {x_1^t, x_2^t, ⋯, x_{I^t}^t} $ ,再映射至 $ J^t $ 个元素的路经集合。即对于 $ ∀x_i^t ∈ x^t, \\mathbf{x}_i^{t+1} \\in \\mathbf{x}^{t+1} $,都构造一条路径。路径集记为 $ \\mathbf{p}^t $,轨迹为:$$ τ = x_1p_1x_2⋯p_{t-1}x_t $$ PIF 会根据车辆轨迹计算概率,最终取概率最高的一条轨迹作为出租车 GPS 序列再地图上匹配得到的轨迹。 数据库函数的编写PostgreSQL 数据库的插件 pgRouting 提供了路径规划的能力。在建立拓扑关系后,可以使用路径规划系列函数求解最短路径。 pgr_dijkstra():使用 Dijkstra 算法求解的最短路径。除此之外,还有求解代价的 Dijkstra 函数。 pgr_aStar():使用 A* 算法求解最短路径。该系列函数即将不受官方支持。 以pgr_dijkstra()为例,它的参数有: 参数名 类型 说明 sql 文本 一个 SQL 查询。 source int4 起点的 ID。 target int4 终点的 ID。 directed boolean 如果地图是有向的,那么为ture。 has_rcost boolean 如果为true,SQL 查询的reverse_cost会被用来计算代价值。 其中 SQL 查询形如 1SELECT id, source, target, cost [,reverse_cost] FROM edge_table 返回列有: 列名 类型 说明 id int4 边的 ID。 source int4 起点的 ID。 target int4 终点的 ID。 cost float 边的代价值。为负则不考虑此边 reverse_cost 可选 边的往返代价值。只在directed和has_rcost参数为true时使用。 但是 pgRouting 提供的函数,只支持从路网数据的一个节点到另一个节点,也就是不支持任意两点间的最短路径。我们需要自己撰写函数来实现这一点。对于两个点 $ p_1,p_2 $,求解其间最短路径 $ p_1p_2$ 实现的思路如下: 找到 $ p_1,p_2 $ 最近的线段 $ l_1, l_2$ 找到 $ l_1 $ 的终点 $p_t$(即target值),和 $ l_2 $ 的起点 $p_s$(即source值)。 求解从target到source的最短路径 $ p $。 将 $ p $ 补上或删除 $ p_1p_t $ 和 $ p_sp_2 $ 两段。 由于在 PIF 中, 要求解路径的点是两个状态值,都在路网上,因此,在搜索最近路段时,搜索范围可放小一点,如 1 m。 根据博客,具体函数实现如下(做了一些修改): 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485CREATE OR REPLACE FUNCTION \"public\".\"pif_getpathbetweenpoints\"(\"startx\" float8, \"starty\" float8, \"endx\" float8, \"endy\" float8) RETURNS TABLE(\"shortest_path\" \"public\".\"geometry\", \"path_cost\" float8) AS $BODY$declare tbl VARCHAR := 'road'; v_point1 geometry := st_transform(st_setsrid(ST_Point(startx, starty), 4326), 3857); v_point2 geometry := st_transform(st_setsrid(ST_Point(endx, endy), 4326), 3857); v_startLine geometry;--离起点最近的线 v_endLine geometry;--离终点最近的线 v_startTarget integer;--距离起点最近线的终点 v_endSource integer;--距离终点最近线的起点 v_statpoint geometry;--在v_startLine上距离起点最近的点 v_endpoint geometry;--在v_endLine上距离终点最近的点 v_res geometry;--最短路径分析结果 v_perStart float;--v_statpoint在v_res上的百分比 v_perEnd float;--v_endpoint在v_res上的百分比 v_shPath geometry;--最终结果 tempnode float; begin --查询离起点最近的线 select geom, target from road where ST_DWithin(geom, v_point1, 3) order by ST_Distance(geom, v_point1) limit 1 into v_startLine ,v_startTarget; --查询离终点最近的线 select geom, source from road where ST_DWithin(geom, v_point2, 3) order by ST_Distance(geom, v_point2) limit 1 into v_endLine,v_endSource; --如果没找到最近的线,就返回null if (v_startLine is null) or (v_endLine is null) then return; end if ; select ST_ClosestPoint(v_startLine, v_point1) into v_statpoint; select ST_ClosestPoint(v_endLine, v_point2) into v_endpoint; --最短路径 execute 'SELECT st_linemerge(st_union(b.geom)) ' || 'FROM pgr_kdijkstraPath( ''SELECT gid as id, source, target, cost FROM road '',' ||v_startTarget || ', ' ||'array['||v_endSource||'] , false, false ) a, ' || tbl || ' b WHERE a.id3=b.gid GROUP by id1 ORDER by id1' into v_res ; --如果找不到最短路径,就返回null --if(v_res is null) then -- return null; --end if; --将v_res,v_startLine,v_endLine进行拼接 select st_linemerge(ST_Union(array[v_res,v_startLine,v_endLine])) into v_res; select ST_LineLocatePoint(v_res, v_statpoint) into v_perStart; select ST_LineLocatePoint(v_res, v_endpoint) into v_perEnd; if(v_perStart > v_perEnd) then tempnode = v_perStart; v_perStart = v_perEnd; v_perEnd = tempnode; end if; --截取v_res SELECT ST_LineSubString(v_res,v_perStart, v_perEnd) into v_shPath; RETURN QUERY SELECT st_transform(v_shPath, 4326), st_length(v_shPath);end$BODY$ LANGUAGE plpgsql VOLATILE COST 100 ROWS 1000 修改的地方: 固定了要查询的表。 将结果转换为 WGS84 坐标系。 在返回路径的同时返回了整个路径的长度。 Python 调用函数的编写与之前类似,Python 调用函数的实现如下: 123456789101112131415def getPaths(self, s1: State, s2: State): ''' 获取两个状态之间的路径 利用ArcPy获取路网上两点之间的最短路径 ''' # 创建要素 if s1.link_id == s2.link_id: return [] cur = self.conn.cursor() # type: psycopg2.extensions.cursor cur.execute(\"SELECT * FROM PIF_GetPathBetweenPoints(%s, %s, %s, %s)\", (s1.gps_pos.lng, s1.gps_pos.lat, s2.gps_pos.lng, s2.gps_pos.lat)) (linestring, cost) = cur.fetchone() # type: LineString path = [((p1.x, p1.y), (p2.x, p2.y)) for (p1, p2) in zip(linestring[1:-2], linestring[2:-1])] del cur return [(s1, [s1.link_id] + path + [s2.link_id], s2, cost)] 路径结果后处理在 PIF 核心库中,对两个车辆状态 $ x_i^{t} \\in \\mathbf{x}^{t} ,x_j^{t+1} \\in \\mathbf{x}^{t+1} $ 中间的路径 $ p_i $ 有要求。即 $ x_i^{t} $ 所在的路段的 ID 和 $x_j^{t+1}$ 所在路段的 ID 分别和 $ p_i $ 第一个路段的 ID 和 最后一个路段的 ID 相同。在不直接使用路段的 ID 作为最短路径搜索的返回值时,我们需要做一些处理。 产生这个问题原因,是 PIF 库的示例代码中,采用 $ ((x_S, y_S), (x_T, y_T)) $ 来表示 $ x_i^{t} $ 所在的路段 ID。若记获取某个状态值所在路段的 ID 的函数是 $ \\mathbf{id}(x) $,则要求 $ \\mathbf{id}(x_i^{t}) = \\overrightarrow{ST} = (p_i)_1 $,$ S, T $ 是路段的起点和终点。同理,$ \\mathbf{id}(x_i^{t}) = \\overrightarrow{ST} = (p_i)_{-1} $($(p_i)_{-1}$ 表示最后一个搜索到的路径的最后一个路段)。 对于起始状态,理论上共有 6 种可能。如下图所示。记起始状态点为 $X$,其可能的 6 种情况分别为 $ \\left\\lbrace x_1, x_2, \\cdots, x_6 \\right\\rbrace $,其所在路段 $ \\mathbf{l} = \\overrightarrow{ST}$ 起点为 $S$,终点为 $T$,匹配到的整个路径为 $ P $。记符号 $ \\lnot \\mathbf{L} = \\overrightarrow{TS} $。 对于每个 $ X \\in \\left\\lbrace x_1, x_2, \\cdots, x_6 \\right\\rbrace $ $ X = x_1 \\neq S \\neq T \\wedge T = P_2 $ :这是最一般的情况。只需令 $ P = \\left\\lbrace S \\right \\rbrace \\cup P $ 即可。 $ X = S \\wedge T = P_2 $ :此种情况无需处理。 $ X = T \\wedge S \\neq P_2 $ :令 $ P = \\left\\lbrace S \\right \\rbrace \\cup P $。 $ X = x_1 \\neq S \\neq T \\wedge S = P_2 $ :令 $ \\mathbf{id}(X) = \\lnot \\mathbf{l} $。 $ X = S \\wedge T \\neq P_2 $ :令 $ P = \\left\\lbrace T \\right \\rbrace \\cup P $,且 $ \\mathbf{id}(X) = \\lnot \\mathbf{l} $。 $ X = T \\wedge S = P_2 $ :令 $ \\mathbf{id}(X) = \\lnot \\mathbf{l} $。 对于结束状态,可能的情况如下图。 处理方法可与上同理。 由于 pgRouting 的问题,好像会给求得的路径按照坐标大小排个序?我目前遇到过一次,因此,在进行上述处理之前,判断一下,起始状态和结束状态的坐标值是不是分别和路径的第一个点和最后一个点相同,如果不是,把路径反过来。 在理论上,上述 6 种情况是合理的。但是在实际运行过程中,竟然产生了 $$ X = x_1 \\neq S \\neq T \\wedge T = P_2 \\wedge S = P_2 $$ 的情况,因此还需要做一个处理。如果上述 6 种情况都不满足,视为没找到路径。 利用 ArcPy 实现调用 PIF 算法","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"路网速度","slug":"路网速度","permalink":"http://hpdell.github.io/tags/路网速度/"}]},{"title":"使用TypeScript编写爬虫","slug":"crawler-cheerio-ts","date":"2017-12-06T21:23:28.000Z","updated":"2022-04-14T16:50:55.409Z","comments":true,"path":"编程/crawler-cheerio-ts/","link":"","permalink":"http://hpdell.github.io/编程/crawler-cheerio-ts/","excerpt":"我们需要的数据多种多样,不可能什么都买,就算有钱,有的数据也不一定能买到。这个时候要获取这些数据,就要靠爬虫了。 爬虫界大佬很多,开源库和框架数不胜数。理论上,凡是能方便连接互联网的编程语言,都适合用来写爬虫,比如C#、Java、JavaScript、Python等,当然还有用R、Matlab这些有点专业特色的语言写爬虫的,甚至用curl和Bash都可以写爬虫,只是好像较少听说过有用C++写爬虫的。我分别用过C#这种编译语言(Java应该类似)和JavaScript这种脚本语言写过多款爬虫,个人总结起来两种语言各有特色: 编译语言每次修改爬虫都要编译,我电脑比较渣,每次编译都要花点时间。但是可以轻松通过类型检查,保证每次GET参数的正确、返回值的直接解析等非常使用的功能。 脚本语言每次修改就可以直接运行了,但是填参数的时候就比较头疼了,需要反复检查有哪些参数、该填什么值,保证请求参数的格式正确。 自从学会了TypeScript之后,我个人就比较喜欢JavaScript和TypeScript语言,结合Node.js,可以达到比较好的写爬虫的效果,结合了编译语言和脚本语言共有的特点。只是搞得脚本有点复杂了,需要声明很多类型,指定变量类型……其实也是一个挺复杂的工作。虽然有点不像脚本,不够简洁,但能够提供类型检查还是省了不少事。 下面记录一下这次使用TypeScript编写爬虫的过程,以备后用。","text":"我们需要的数据多种多样,不可能什么都买,就算有钱,有的数据也不一定能买到。这个时候要获取这些数据,就要靠爬虫了。 爬虫界大佬很多,开源库和框架数不胜数。理论上,凡是能方便连接互联网的编程语言,都适合用来写爬虫,比如C#、Java、JavaScript、Python等,当然还有用R、Matlab这些有点专业特色的语言写爬虫的,甚至用curl和Bash都可以写爬虫,只是好像较少听说过有用C++写爬虫的。我分别用过C#这种编译语言(Java应该类似)和JavaScript这种脚本语言写过多款爬虫,个人总结起来两种语言各有特色: 编译语言每次修改爬虫都要编译,我电脑比较渣,每次编译都要花点时间。但是可以轻松通过类型检查,保证每次GET参数的正确、返回值的直接解析等非常使用的功能。 脚本语言每次修改就可以直接运行了,但是填参数的时候就比较头疼了,需要反复检查有哪些参数、该填什么值,保证请求参数的格式正确。 自从学会了TypeScript之后,我个人就比较喜欢JavaScript和TypeScript语言,结合Node.js,可以达到比较好的写爬虫的效果,结合了编译语言和脚本语言共有的特点。只是搞得脚本有点复杂了,需要声明很多类型,指定变量类型……其实也是一个挺复杂的工作。虽然有点不像脚本,不够简洁,但能够提供类型检查还是省了不少事。 下面记录一下这次使用TypeScript编写爬虫的过程,以备后用。 库的使用主要使用了下面两个库: web-request cheerio WebRequest:同步化的request请求为什么要用await同步化request请求呢?不仅仅是因为await关键字是TypeScript和新JavaScript标准的特性,其实有很多比较好的用途: 避开JavaScript“回调大坑”。我个人还挺喜欢JavaScript以回调的方式异步化同步操作,曾经在爬虫的时候写了一个全回调的爬虫,fs库中有同步版本的函数都没有用其同步版本,全都是异步版本。事实证明,在高速爬虫的时候,使用异步版本的函数确实提高了爬虫效率。但是,编写起来那个痛苦啊。如果需要从返回的值里面判断还有没有下一页了,就要不停地递归啊。这真的是回调大坑,难读、难写、难调试。以至于后来再写爬虫,都避免爬虫过程中判断是否有下一页,都是提前算好有多少页,然后硬编码的,这样反而会节省很多时间。但是使用同步化的过程就比较好实现了。 减少因JavaScript的回调和闭包造成的错误。再爬虫的时候,为了防反爬,最简单的办法是人工设定等待时间,让爬虫慢一点。由于JavaScript回调的特点,笔者多次尝试,发现只能使用setInterval()函数实现。但是使用TypeScirpt的await关键字,可以直接编写一个delay()函数,让程序等待。 这个库的大多数用法和request库差不多,配置也是直接采用的request库的配置,只是可以以同步的方式编写异步代码,姑且称之为“同步化”把。例如,获取一个get请求的相应就是: 12import * as WebRequest from 'web-request'var list_response = await WebRequest.get(`${site.url}/pg${i + 1}/`); 这里面使用site.url变量存储要访问的网页的基本地址,后面代表了页数。 全局配置的方法也和request库差不多,只是不需要返回一个新的request对象 1WebRequest.defaults({jar: true}) 这里配置了使用Cookie。 我在上一次写高德API爬虫的时候,首次依靠同步化的request请求,完成了自动分析页数。瞬间感觉给人生节约了很多时间。 cheerio:提供jQuery Selector的解析能力在做前端的时候,定位一个元素,最常用的就是jQuery的Selector字符串。在Node.js中,可以使用cheerio这个简化的jQuery库来实现这一操作。 使用cheerio的方法很简单,就三步: 导入cheerio包: 1import * as cheerio from 'cheerio' 创建$对象(body变量代表了HTML响应正文) 1var $ = cheerio.load(body); 使用jQuery Selector即可(info_object.max_num是用于存储某个值的变量) 1info_object.max_num = parseInt($(\"body div.content div.leftContent div.resultDes.clear h2.total.fl span\").text().trim()); 一般大型网页的页面都非常复杂,仅仅依靠分析HTML源码,可能毫无头绪。但是,使用浏览器的开发人员工具就非常方便啦,不仅可以直接快速定位页面元素,还可以直接给出Selector表达式。 爬虫软件“八爪鱼”使用的是XPath表达式来定位页面元素(至少他的软件UI是这样做的)。我也尝试使用XPath,但是,由于HTML一些随意性,往往导致解析出错。而且,既然是网页,使用jQuery Selector表达式更简洁,更合适。 也有人说可以直接使用正则。确实可以,但是正则还要自己想是不是,如果有些复杂的正则还是挺费事的。用这个可以让开发人员工具自动分析,应该是更方便啊,除非你的电脑只有命令行。 当然,也不是什么都需要使用Selector表达式来定位的。如果只是像把坐标提出来,或者有一些其他特定的模式,直接使用正则表达式啊,比如 1var coord_string = /114.[0-9]*,30.[0-9]*/g.exec(body)[0]; 这样就提取出来了经纬度坐标,而这个坐标是隐藏在页面一个script标签中的一个变量里。 不过这种方式有点问题。如果页面上的某些标签是使用脚本添加的,可能开发人员工具给出的Selector路径,不一定能在HTML源代码里找到。但是如果很重要的数据是通过前端脚本渲染上去的,那肯定会在一个变量里面保存这些数据。这个时候直接揪出这个变量就可以了,万事大吉,还不用自己去提取HTML元素。 TypeScript编写爬虫既然使用TypeScript便写爬虫,那么就使用一些TypeScript的特性吧。首先应该就是TypeScript的类型化特点。当然还少不了await关键字。 API参数的类型化一个请求的请求参数往往是确定的,在一个开放API中都会给出。你所想要的数据类型是固定的,这个要看你的需求。类型化编写爬虫的方式,就是保证这两个过程不出错。 例如,在利用高德API获取POI的时候,参数在文档中明确指出了(高德地图API文档)。我们如果照着这样的文档,编写一个接口或者一个类,可以实现一些自动化功能。同样,返回结果也可以编写一个类型,直接在构造函数中实现一些对结果的处理。 例如,对高德搜索的API进行类型化。首先创建一个接口,表示一些除了key之外的参数 123456789101112131415161718/** 输出结果的格式 */export enum GaodePoiOutput { JSON, XML}/** 接口参数类型 */export interface IGaodePoiApi { keywords: string[]; types: string[]; city?: string; citylimit?: boolean; children?: number; offset?: number; page?: number; building?: number; floor?: number; extensions?: string; output?: GaodePoiOutput;} 然后,编写一个类,让其提供自动根据上述参数类型生成url的功能 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455export class GaodePoiApi { baseurl: string; key: string; constructor(key: string) { this.baseurl = \"http://restapi.amap.com/v3/place/text\" this.key = key; } /** * 获取参数指定的POI * @param parameters 请求参数 */ getUrl(parameters: IGaodePoiApi): string { var url = `${this.baseurl}?key=${this.key}`; for (var key in parameters) { if (parameters.hasOwnProperty(key)) { switch (key) { case \"keywords\": if (parameters.keywords.length > 0) { url += `&${key}=`; url += parameters.keywords[0] for (var index = 1; index < parameters.keywords.length; index++) { var element = parameters.keywords[index]; url += `|${parameters.keywords[index]}`; } } break; case \"types\": if (parameters.types.length > 0) { url += `&${key}=`; url += parameters.types[0] for (var index = 1; index < parameters.keywords.length; index++) { var element = parameters.keywords[index]; url += `|${parameters.keywords[index]}` } } break; case \"output\": url += `&${key}=`; switch (parameters.output) { case GaodePoiOutput.XML: url += \"XML\"; break; default: url += \"JSON\"; } default: url += `&${key}=`; url += `${parameters[key]}`; break; } } } return url; }} 这样直接调用GaodePoiApi对象的getUrl()函数即可生成需要的请求。 为什么不用request库的qs配置参数呢?这个参数其实非常坑,在高德API、百度API这种特别复杂的请求参数要求下,往往会出现问题。例如,如果一个参数可以是一个数组,你却不能直接把这个参数的值写成数组,这样会出现问题,只能手动利用将其变成字符串。我一开始使用的是querystring.stringify()函数,直接将qs对象序列化成字符串,但是后来发现还是有问题。但是使用TypeScript这样做之后,就觉得更加合理,一些复杂的参数格式也更加可控。 请求结果的类型化开放API的示例如在爬取高德地图API的时候,返回一个JSON时,可以直接将其指定一个类型,方便后面的操作。例如: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859/** 高德POI搜索结果模型 */export interface IGaodePoiSearchResultModel { status: string; info?: string; infocode?: string; count?: string; pois?: IGaodePoiModel[]; suggestion?: IGaodePoiSearchSuggestionsModel[];}/** 高德地图API */export class GaodePoi { id: string; name: string; typecode: string; biz_type: any[]; address: string; gjclng: number; gjclat: number; wgslng: number; wgslat: number; tel: string; distance: any[]; biz_ext: any[]; pname: string; cityname: string; adname: string; importance: any[]; shopid: any[]; shopinfo: string | number; poiweight: any[]; constructor(parameters: IGaodePoiModel) { // 复制属性 this.id = parameters.id; this.name = parameters.name; this.typecode = parameters.typecode; this.biz_type = parameters.biz_type; this.address = parameters.address; this.tel = parameters.tel; this.distance = parameters.distance; this.biz_ext = parameters.biz_ext; this.pname = parameters.pname; this.cityname = parameters.cityname; this.adname = parameters.adname; this.importance = parameters.importance; this.shopid = parameters.shopid; this.shopinfo = parameters.shopinfo; this.poiweight = parameters.poiweight; // 计算坐标 var coords = parameters.location.split(\",\"); this.gjclng = parseFloat(coords[0]) this.gjclat = parseFloat(coords[1]) var wgs = coordtransform.gcj02towgs84(this.gjclng, this.gjclat) this.wgslng = wgs[0]; this.wgslat = wgs[1]; } static getFields(): string[] { return [ \"id\", \"name\", \"typecode\", \"biz_type\", \"address\", \"gjclng\", \"gjclat\", \"wgslng\", \"wgslat\", \"tel\", \"distance\", \"biz_ext\", \"pname\", \"cityname\", \"adname\", \"importance\", \"shopid\", \"shopinfo\", \"poiweight\" ] }} 这里面在构造函数中,调用了坐标转换库,转换了获取到的坐标。这个过程在获取结果时,在构造的过程中自动调用。 具体程序示例请参考:download-gaode-poi HTML页面的示例如果不是爬开放API,类型化也有一定的作用。例如爬取列表的时候,或者详细信息的时候,可以知道哪些属性时需要爬的,以及还有哪些属性没有爬下来。例如这段类型的声明 123456789101112131415161718192021222324export class ErshoufangListItem { id: string; title: string; link: string; property: string; propertylink: string; tags: Array<string>; total_price: number; unit_price: number; type_time: string; constructor(self?: ErshoufangListItem) { this.tags = new Array<string>(); if (self) { this.title = self.title; this.link = self.link; this.property = self.property; this.propertylink = self.propertylink; this.tags = self.tags; this.total_price = self.total_price; this.unit_price = self.unit_price; this.type_time = self.type_time; } }} 对应了下面这个列表页面中所需要提取的数据 在爬取页面的时候,按照这个类型中声明的属性进行爬取即可。这一点可以用于多人协作中,一个人负责确定所要爬取的数据的原型(声明这个类),另一个人便写爬虫,其他人按照这个类型对数据进行分析。 延时函数编写如果想让程序等待一定时间再继续爬取,setInterval()函数是可以使用的,但是又容易掉到回调坑里面。如果你再一个请求得到返回结果后又发起了一系列请求,这样两套setInterval()是统一不起来的,各计各的时间(因为request也用的是回调)。这个时候用TypeScript的await关键字调用一个延时函数(起名为delay())是再好不过的。 delay()函数如下: 1234567/** * 延时函数 * @param times 延时时间 */function delay(times: number): Promise<void>{ return new Promise<void>((resolve, reject)=>{setTimeout(()=>resolve(), times)});} 使用时直接用await关键字“调用”即可 1await delay(10000); “前端爬虫”的后台搭建像百度地图API这种开放平台,有的时候JavaScript API提供的功能比Web服务API提供的功能多。例如,JavaScript API提供了“商圈”数据获取的功能。如果我们要爬取商圈数据,那就只在HTML页面中,调用这套API,将获取到的数据,通过Ajax发送到服务器上。此时要求服务器需要提供上传数据的接口,一旦这个接口被访问,服务器将前端上传的数据保存到文件中即可。 之所以这样做,是因为浏览器一般没有直接操作本地文件的能力,不能再获取到数据之后直接保存成文件。如果真要直接保存,那可能只能保存成cookie或者“本地存储”之类的东西?这样又不是很好用。 这样一个爬虫的分工就更明显了。前端工程师可以在前端设计页面如何自动调用API进行数据获取并提交,后端工程师设计服务器接口以进行数据的接收、处理和存储。 例如在编写这个商圈数据爬虫的过程中,百度给的示例页面是这样的: 使用的是百度提供的CityList类,包含两个方法: getBussiness():获取商圈数据。 getSubAreaList():获取下级的区域列表。 通过前端不断调用getBussiness()方法,即可获取到不同商圈的参数。 123456789101112131415161718192021222324252627282930313233for (let j = 0; j < all_business.length; j++) { const element = all_business[j]; console.log(element, \"商圈数据\"); await new Promise(function (resolve, reject) { cityList.getBusiness(element, async function (json) { await new Promise(function (resolve, reject) { $.ajax({ url: \"/upload/businessCircle\", type: \"POST\", data: { body: JSON.stringify({ name: element, business: json, city: \"武汉市\" }) }, success: function (body) { resolve(); }, error: function () { reject(); } }) }) resolve(); }) }) await new Promise(function (resolve, reject) { setTimeout(function () { resolve() }, 1000) })} 这段程序代码中: 由于ES6带来了await,前端现在也可以使用这种方式来使异步执行的程序同步化。但同样要求在async修饰的函数中才能使用await关键字。 all_business是所有商圈的名字。不同城市可能有相同名字的商圈,比如“中山公园”,这时可以根据返回结果中商圈的“城市”字段在判断。 JSON.stringify()POST参数是“Key-Value”模式的,因此一个键对应一个值,这个值用字符串形式。如果直接传入一个对象,会被转换成多个键值对的形式。所以遇到JS对象,就要用JSON.stringify()函数将其变成字符串,在后台再用JSON.parse()函数解析。 POST参数可以使用TypeScript的interface进行建模,这就需要浏览器中的脚本也使用TypeScript编写,然后编译。对于前端来说使用TypeScript的意义可能不大,因为很多前端库没有TypeScript的声明文件。但是前端工程师可以将自己所采用的POST参数模型交给后端工程师,后端工程师按照这个模型进行处理。 在后端,建立一个接受POST请求的服务,比如我用Express搭建的服务器,提供了这样一个POST接口: 123456789101112131415161718192021222324/** * 浏览器提交商圈 * @param req 请求 * @param res 响应 * @param next 后处理函数 */function postBusinessCircle(req: express.Request, res: express.Response, next: express.NextFunction) { var data: {name: string, city: string, business: BusinessCircle[]} = JSON.parse(req.body.body); fs.writeFile(`data/BusinessCircle/${data.name}.json`, JSON.stringify({ name: data.name, business: data.business.filter(x => x.city === data.city) }), (err) => { if (err) console.log(err); else { console.log(`${data.name}写文件完成`); res.send(\"success\") } })}var router = express.Router();router.post(\"/businessCircle\", postBusinessCircle)module.exports = router; 即可接收前端通过POST参数上传的数据。 具体程序示例请参考:baidu-business-circle。 其他的话抓包工具要爬虫一定无法避免抓包。一般浏览器的开发人员工具又抓包的功能,同样也可以使用一些抓包工具来抓包。我比较喜欢使用抓包工具Fiddler。 使用浏览器自带的抓包工具,只能在当前页面抓包,而且如果新弹出了一个窗口,往往需要打开抓包工具后刷新一下页面才能抓到包。抓包的结果不能保存,不太方便。 使用Fiddler抓包就比较有优势,可以克服上述问题。但是Fiddler抓包范围太广,有些其他程序的http/https请求也会被抓到,因此抓包的结果可能要多很多。这个时候就要仔细分析哪些包是需要的,哪些包是不需要的。分析起来难度增大俩。 其他抓包工具我还没有试过,用过Fiddler之后感觉确实挺好用的,所以就没有试其他的了。 Fiddler还可以抓手机上的包,只需要设置代理即可,我曾经用这种方法抓了参考消息App的包,分析出它的API。如果一个手机软件用的是HTTPS协议,装一下Fiddler的证书即可。当然这时最好还是在安卓模拟器里面安装,以防个人信息无意中泄露。 SourceMap选项如果使用VSCode编写的话,可以直接调试。直接调试JavaScript是可以的,但是如何调试TypeScript呢?毕竟tsc编译生成的JS脚本太复杂了。 这需要在tsconfig.json文件和.vscode/launch.json中,分别开启sourceMap选项和sourceMaps选项。 123456789// tsconfig.json{ \"compilerOptions\": { \"lib\": [ \"es2015\" ], \"sourceMap\": true }} 12345678910111213141516171819// .vscode/launch.json{ // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 \"version\": \"0.2.0\", \"configurations\": [ { \"type\": \"node\", \"request\": \"launch\", \"name\": \"Launch Program\", \"program\": \"${file}\", \"sourceMaps\": true, \"outFiles\": [ \"${workspaceFolder}/**/*.js\" ] } ]} Visual Studio Code 的插件VSCode有一个“JSON to TS”的插件,在使用起来非常方便。这个插件可以根据剪贴板或JSON文件中的JSON字符串,按照其格式,生成对应的TypeScript Interface。这样,如果爬取一些给出了示例JSON数据的开放API,或者是前端工程师提供的示例数据,都可以直接使用这个插件生成Interface,非常方便开发。 不过这样生成的Interface也不是万能的,需要手动修改一些地方。如一些Interface的名字等。 暂时先记录到这里了。如果日后发现有一些需要补充的还会再添加上。如有错误欢迎大家指正。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"爬虫","slug":"爬虫","permalink":"http://hpdell.github.io/tags/爬虫/"},{"name":"TypeScript","slug":"TypeScript","permalink":"http://hpdell.github.io/tags/TypeScript/"}]},{"title":"手机中的信息安全","slug":"information-security-on-mobile","date":"2017-11-28T19:45:24.000Z","updated":"2022-04-14T16:50:55.457Z","comments":true,"path":"其他/information-security-on-mobile/","link":"","permalink":"http://hpdell.github.io/其他/information-security-on-mobile/","excerpt":"","text":"","categories":[{"name":"其他","slug":"其他","permalink":"http://hpdell.github.io/categories/其他/"}],"tags":[{"name":"课程","slug":"课程","permalink":"http://hpdell.github.io/tags/课程/"}]},{"title":"WPF 给列表加上自动编号","slug":"list-with-number","date":"2017-08-24T17:56:10.000Z","updated":"2022-04-14T16:50:55.517Z","comments":true,"path":"编程/list-with-number/","link":"","permalink":"http://hpdell.github.io/编程/list-with-number/","excerpt":"前言给 WPF 列表添加自动编号是个非常头疼的问题。综合网上的解决方案,有几种: 使用数据绑定。即给数据对象加入一个表示编号的属性,利用数据绑定显示编号。此方法实现方便,但是难以自动更新。 使用代码。在网上看到了这样一种解决方法,文章很多,比如这里,看起来比较麻烦,我们目标是寻找一种纯 XAML 的解决方案。 还有一个使用 VB.Net 写的代码示例(WPF中给listboxItem加上序号标签),声称可以。但是我不太了解 VB,不清除具体情况。 还有一个方案看起来是非常简单的,也是我所采用的解决方案。最早没有成功实现,后来参考了《WPF 编程宝典》才成功实现。但是现在一时半会儿找不到了。","text":"前言给 WPF 列表添加自动编号是个非常头疼的问题。综合网上的解决方案,有几种: 使用数据绑定。即给数据对象加入一个表示编号的属性,利用数据绑定显示编号。此方法实现方便,但是难以自动更新。 使用代码。在网上看到了这样一种解决方法,文章很多,比如这里,看起来比较麻烦,我们目标是寻找一种纯 XAML 的解决方案。 还有一个使用 VB.Net 写的代码示例(WPF中给listboxItem加上序号标签),声称可以。但是我不太了解 VB,不清除具体情况。 还有一个方案看起来是非常简单的,也是我所采用的解决方案。最早没有成功实现,后来参考了《WPF 编程宝典》才成功实现。但是现在一时半会儿找不到了。 代码实现《WPF 编程宝典》中介绍了“条纹列表”样式的实现。样子大概如下: 条纹列表实现条纹列表,需要用到 ListBox/ListView 控件的 AlternationCount 属性。MSDN对该属性的解释如下: 获取或设置 ItemsControl 中的交替项容器的数目,该控件可使交替容器具有唯一外观。 交替项容器的效果,就是对每一个 ListBoxItem/ListViewItem 有一个 ItemsControl.AlternationIndex 属性,表示该项的交替位置。例如,如果对一个 ListBox/ListView 控件设置 AlternationCount 属性为 3,则每一项的索引号和交替项索引号如下: 列表索引 交替项索引 0 0 1 1 2 2 3 0 4 1 5 2 6 0 7 1 于是,根据交替项索引,就可以实现条纹列表。在数据模板中,使用 RelativeSource 找到当前数据所在的 ListBoxItem,使用 (ItemsControl.AlternationIndex) 属性获取其交替项索引,对不同的索引进行处理。或者在生成的项的样式模板中,使用触发器修改条目样式,原书代码如下: 编号列表将上述方法进行推广,即可得到编号列表。我们可以将 AlternationCount 属性设置为列表项目的总数,这样在 ItemsControl.AlternationIndex 属性中,就可以获取当前列表项在列表中的位置。 数据绑定在数据模板中,将一个 TextBlock 的 Text 属性绑定到该属性中,即可进行显示。 12<TextBlock Text=\"{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListBoxItem}, Path=(ItemsControl.AlternationIndex)\" /> 转换器当然,该位置是从 0 开始的,一般我们习惯于从 1 开始。因此,需要一个转换器。既然使用了转换器,就可以自定义很多表示方法,如“周日”、“周一”……“周六”;“第1名”、“第2名”…… 例如如下设计的一个转换器: 12345678910111213141516171819202122class IntToLevelStringConverter : IValueConverter{ static string[] LevelString = { \"第一级\", \"第二级\", \"第三级\", \"第四级\", \"第五级\" }; public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { int levelRank = (int)value; if (levelRank < LevelString.Length) { return LevelString[levelRank]; } else { return null; } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); }} 使用图片指示等级或者使用图片指示等级,如 那么,XAML 代码中应创建一个 Image 控件,将其 Source 属性绑定到 ItemsControl.AlternationIndex 属性,利用转换器转换成对应图片的 URL。 XAML的代码: 123<Image Source=\"{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListBoxItem}, Path=(ItemsControl.AlternationIndex), Converter={StaticResource IntToIconStringConverter}}\"/> 转换器的代码: 12345678910111213141516171819202122232425262728class IntToIconStringConverter : IValueConverter{ static string[] IconPath = { \"pack://application:,,,/FGISClient.UIControls;component/Icon/FireHandle/1ji.png\", \"pack://application:,,,/FGISClient.UIControls;component/Icon/FireHandle/2ji.png\", \"pack://application:,,,/FGISClient.UIControls;component/Icon/FireHandle/3ji.png\" }; public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { int levelRank = (int)value; if (levelRank < IconPath.Length) { return IconPath[levelRank]; } else { return null; } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); }}","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"WPF","slug":"WPF","permalink":"http://hpdell.github.io/tags/WPF/"},{"name":"XAML","slug":"XAML","permalink":"http://hpdell.github.io/tags/XAML/"}]},{"title":"WPF制作带居中三角形指示的Tooltip样式的Tooltip","slug":"WPF-Bootstrap-Tooltip","date":"2017-08-21T22:19:30.000Z","updated":"2022-04-14T16:50:55.409Z","comments":true,"path":"编程/WPF-Bootstrap-Tooltip/","link":"","permalink":"http://hpdell.github.io/编程/WPF-Bootstrap-Tooltip/","excerpt":"本文所要实现的目标样式如下图。 可以看到这个Tooltip能够分解为一个三角形和一个圆角矩形,而且三角形要居中显示。经过在网上的充分搜索,没有找到可以直接使用的解决方案,那么就自己动手设计一个。","text":"本文所要实现的目标样式如下图。 可以看到这个Tooltip能够分解为一个三角形和一个圆角矩形,而且三角形要居中显示。经过在网上的充分搜索,没有找到可以直接使用的解决方案,那么就自己动手设计一个。 布局框架既然要居中显示一个三角形,最方便的应该就是Gird布局了。因此使用XAML创建一个Grid布局。 12<Grid x:Name=\"g\"></Grid> 由于三角形和圆角矩形在某种程度上说是结合在一起的,因此无需设置行和列。所以这里使用一个Border来布局也是可以的。 内部布局我们希望使用三角形“盖住”一部分圆角矩形的边框,因此需要将三角形放置在圆角矩形的下方,才能实现遮盖的效果。 除此之外还有以下要求: 采用Canvas面板来绘制三角形,此面板需要水平居中对齐、垂直顶部对齐;假设三角形高为6,宽为12,为等腰三角形。 采用Border控件实现圆角矩形,需要有一定边框宽度和颜色,水平拉伸、垂直拉伸,且与Grid面板的上边缘有一定的边距,边距大小略小于三角形高,这样可以让三角形遮盖一段边框。 因此采用如下设计: 12345678910111213<Grid x:Name=\"g\"> <Border CornerRadius=\"3\" BorderThickness=\"1\" BorderBrush=\"{StaticResource TooltipBorderBrush}\" HorizontalAlignment=\"Stretch\" VerticalAlignment=\"Stretch\" Background=\"White\" Margin=\"0,5,0,0\" Padding=\"8\"> </Border> <Canvas HorizontalAlignment=\"Center\" VerticalAlignment=\"Top\" Height=\"6\" Width=\"12\"> </Canvas></Grid> 三角形绘制在一个Canvas面板中,使用Polygon绘制三角形,设置为白色。那么这个三角形三个点的坐标当然就是$(0,6)$、$(6,0)$、$(12,6)$了。这样就能绘制一个三角形,又遮盖一段圆角矩形的边框。 1<Polygon Points=\"0,6 6,0 12,6\" StrokeThickness=\"0\" Fill=\"White\"/> 三角形的边框不能直接使用属性进行设置了,否则三角形的底也会被绘制上边框,无法达到效果。我们可以绘制一段多段线(Polyline)来实现边框的绘制。同样还是设置上面三个点,但是对象类型改为Polyline。 123<Polyline Points=\"0,6 6,0 12,6\" Stroke=\"{StaticResource TooltipBorderBrush}\" StrokeThickness=\"1\"/> 此时三角形绘制完成。 注意:需要明确指定Canvas面板的宽度,此处为12。否则,三角形无法真正居中,而是最左边对齐到圆角矩形的中间。 圆角矩形的设置圆角矩形样式较好设置。内容的设置可以使用ContentPresenter,使得在使用时可以直接使用XAML代码来设置内容。该Presenter设置为水平居中、垂直居中即可。 1<ContentPresenter VerticalAlignment=\"Center\" HorizontalAlignment=\"Center\"/> 整体代码1234567891011121314<Grid x:Name=\"g\"> <Border CornerRadius=\"3\" BorderThickness=\"1\" BorderBrush=\"{StaticResource TooltipBorderBrush}\" HorizontalAlignment=\"Stretch\" VerticalAlignment=\"Stretch\" Background=\"White\" Margin=\"0,5,0,0\" Padding=\"8\"> <ContentPresenter VerticalAlignment=\"Center\" HorizontalAlignment=\"Center\"/> </Border> <Canvas HorizontalAlignment=\"Center\" VerticalAlignment=\"Top\" Height=\"6\" Width=\"12\"> <Polygon Points=\"0,6 6,0 12,6\" StrokeThickness=\"0\" Fill=\"White\"/> <Polyline Points=\"0,6 6,0 12,6\" Stroke=\"{StaticResource TooltipBorderBrush}\" StrokeThickness=\"1\"/> </Canvas></Grid> 可以将此段代码放置在自定义Tooltip样式的Template属性值下,实现通过样式进行设置。即 123456789101112131415161718192021222324252627282930313233<Style x:Key=\"FGisToolTipStyle\" TargetType=\"ToolTip\"> <Setter Property=\"OverridesDefaultStyle\" Value=\"true\" /> <Setter Property=\"HasDropShadow\" Value=\"True\" /> <Setter Property=\"Foreground\" Value=\"#333333\" /> <Setter Property=\"FontSize\" Value=\"14\" /> <Setter Property=\"Placement\" Value=\"Bottom\"/> <Setter Property=\"Template\"> <Setter.Value> <ControlTemplate TargetType=\"ToolTip\"> <Grid x:Name=\"g\"> <Border CornerRadius=\"3\" BorderThickness=\"1\" BorderBrush=\"{StaticResource TooltipBorderBrush}\" HorizontalAlignment=\"Stretch\" VerticalAlignment=\"Stretch\" Background=\"White\" Margin=\"0,5,0,0\" Padding=\"8\"> <ContentPresenter VerticalAlignment=\"Center\" HorizontalAlignment=\"Center\"/> </Border> <Canvas HorizontalAlignment=\"Center\" VerticalAlignment=\"Top\" Height=\"6\" Width=\"12\"> <Polygon Points=\"0,6 6,0 12,6\" StrokeThickness=\"0\" Fill=\"White\"/> <Polyline Points=\"0,6 6,0 12,6\" Stroke=\"{StaticResource TooltipBorderBrush}\" StrokeThickness=\"1\"/> </Canvas> </Grid> </ControlTemplate> </Setter.Value> </Setter></Style> 也可以放置在自定义的用户控件中,作为控件使用。","categories":[{"name":"编程","slug":"编程","permalink":"http://hpdell.github.io/categories/编程/"}],"tags":[{"name":"WPF","slug":"WPF","permalink":"http://hpdell.github.io/tags/WPF/"},{"name":"XAML","slug":"XAML","permalink":"http://hpdell.github.io/tags/XAML/"}]}]}