使用 Workerman 接入 Bilibili 直播弹幕协议

逛 B 站的时候,突然想到可以用 PHP 接入直播弹幕,然后在命令行显示弹幕消息。

经过搜索发现了一篇讲解 Bilibili 直播弹幕协议的文章(链接在文末),通过这篇文章了解到了弹幕的协议格式以及大致的流程,开发过程中遇到的一些问题参考了弹幕姬的解决思路。

本文源码的 GitHub 地址:http://github.com/her-cat/bilibili-barrage

转载请注明来源地址:http://her-cat.com/posts/2021/04/01/workerman-to-access-bilibili-barrage-protocol/

弹幕协议的介绍

弹幕协议由头部和数据组成,头部的长度是固定的 16 字节,数据的长度 = 数据包总长度 - 头部的长度。

协议的字节序均为大端模式。高字节在低地址,低字节在高地址,比如 0x1234,在大端模式下存储是 0x12 0x34,在小端模式下是 0x34 0x12。

弹幕协议图示

下面是弹幕协议的格式。

字段对照表:

字段 含义
packet_len 数据包的总长度
header_len 头部长度(固定为 16 字节)
version 协议版本号(默认为 2)
opcode 操作码,用来标识数据包的类型(详情见下表)
magic_number 魔术数字(默认为 1)
data 携带的数据,长度 = packet_len - header_len

已知的操作码:

操作码 常量 含义
2 Opcode::CLIENT_HEARTBEAT 客户端发送的心跳包
3 Opcode::POPULARITY_VALUE 人气值,数据是 4 字节整数
5 Opcode::CMD 命令,数据中['cmd']表示具体命令(见下表)
7 Opcode::AUTHENTICATION 认证并加入房间
8 Opcode::SERVER_HEARTBEAT 服务器发送的心跳包

已知的命令:

命令 常量 含义
INTERACT_WORD CMD::INTERACT_WORD 进入直播间
DANMU_MSG CMD::DANMU_MSG 弹幕消息
SEND_GIFT CMD::SEND_GIFT 送礼物
COMBO_SEND CMD::COMBO_SEND 连续送礼物
NOTICE_MSG CMD::NOTICE_MSG 通知消息
ONLINE_RANK_V2 CMD::ONLINE_RANK_V2 在线 PK

常量列是对应的值在代码中的常量名。

处理弹幕协议

跟协议相关的操作都放在了 Packet 类中,将一些固定的值设置成了类的常量。

/**
 * 头部长度
 */
const HEADER_LEN = 16;

/**
 * 协议版本
 */
const PROTOCOL_VERSION = 2;

/**
 * 魔法数字,设置为 1 即可
 */
const MAGIC_NUMBER = 1;

打包协议

先来看看打包弹幕协议的逻辑,先计算出数据包的总长度,然后将头部信息及数据打包成二进制数据。

public static function pack($opcode, $payload = '')
{
    $packetLen = static::HEADER_LEN;
    if (!empty($payload)) {
        $packetLen += strlen($payload);
    }

    return pack('NnnNN', $packetLen, static::HEADER_LEN, static::PROTOCOL_VERSION, $opcode, static::MAGIC_NUMBER).$payload
}

pack/unpack 函数

这里简单讲下 pack/unpack 函数的使用。

pack 就是将输入参数打包成指定格式二进制数据,上面的 n、N 就是指定的格式,分别表示无符号短整型(16位,大端字节序)无符号长整型(32位,大端字节序)

第一个 N 就是以 无符号长整型(32位,大端字节序) 的格式打包 数据包总长度。
第二个 n 就是以 无符号短整型(16位,大端字节序) 的格式打包 头部长度。
第三个 n 就是以 无符号短整型(16位,大端字节序) 的格式打包 协议版本号。
后面的以此类推...

上面使用的是 PHP 可变参数的方式进行打包,也可以将每个数据单独打包最后再拼在一起,效果也是一样的。

return sprintf(
    '%s%s%s%s%s%s',
    pack('N', $packetLen),
    pack('n', static::HEADER_LEN),
    pack('n', static::PROTOCOL_VERSION),
    pack('N', $opcode),
    pack('N', static::MAGIC_NUMBER),
    $payload
);

更多的介绍可以看 http://www.php.net/manual/zh/function.pack.php

unpack 就是 pack 的反向操作,根据指定的格式将二进制数据解压到数组中。

每条数据以 指定的格式 + key 的方式组成,多条数据用 / 分隔。

举个例子:

$data = pack('Nnn', 2021, 3, 31);

var_dump($data);

$arr = unpack('Nyear/nmonth/nday', $data);

var_dump($arr);

// 输出:

string(8) "\000\000�\000\000"
array(3) {
  'year' => int(2021)
  'month' =>int(3)
  'day' => int(31)
}

打包的时候是按照 Nnn 的格式打包的,所以解压的时候也是按照 Nnn 的格式来的,只不过需要在每个格式的右边指定以这个格式解压出来的数据对应的 key 是什么。

Nyear 就是以 无符号长整型(32位,大端字节序) 的格式解压,并将 year 作为该数据的 key。
nmonth 就是以 无符号短整型(16位,大端字节序) 的格式解压,并将 month 作为该数据的 key。
...

解压弹幕协议

接下来看看解压弹幕协议的逻辑,其实跟上面说的一样,按照打包的顺序然后指定对应的 key 就可以了。

public static function unpack($data)
{
    if (empty($data)) {
        return [];
    }

    return unpack('Npacket_len/nheader_len/nprotocol_version/Nopcode/Nmagic_number/a*payload', $data);
}

a 表示字符串,* 表示任意长度,更严谨一点应该将 * 改为数据的长度( 数据包总长度 - 头部长度)

使用 Node.js 处理协议

这篇文章发出来之后,我试着用 Node.js 来处理弹幕协议,发现写起来是真的舒服。

const PACKET_HEADER_LEN = 16;
const PACKET_PROTOCOL_VERSION = 2;
const PACKET_MAGIC_NUMBER = 1;

class Packet {
    static pack(opcode, payload = '') {
        let packet_len = PACKET_HEADER_LEN;
        if (payload.length > 0) {
            packet_len += payload.length;
        }

        let buffer = Buffer.alloc(packet_len);

        buffer.writeInt32BE(packet_len, 0);
        buffer.writeInt16BE(PACKET_HEADER_LEN, 4);
        buffer.writeInt16BE(PACKET_PROTOCOL_VERSION, 6);
        buffer.writeInt32BE(opcode, 8);
        buffer.writeInt32BE(PACKET_MAGIC_NUMBER, 12);

        if (payload.length > 0) {
            buffer.write(payload, PACKET_HEADER_LEN, payload.length);
        }

        return buffer;
    }

    static unpack(data) {
        let buffer = Buffer.from(data);

        return {
            packet_len: buffer.readInt32BE(0),
            header_len: buffer.readInt16BE(4),
            version: buffer.readInt16BE(6),
            opcode: buffer.readInt32BE(8),
            magic_number: buffer.readInt32BE(12),
            data: buffer.slice(PACKET_HEADER_LEN),
        };
    }
}

与弹幕服务器的交互

接下来看看如何通过弹幕服务器的认证,并在加入房间之后维护在线状态,我将这部分逻辑都放在了 BilibiliBarrage 类中。

获取弹幕服务器信息

在连接弹幕服务器之前,需要通过房间 id 获取到弹幕服务器的地址和端口号,还有认证需要用到的 token。

 const CHAT_CONFIG_URL = 'http://api.live.bilibili.com/room/v1/Danmu/getConf?room_id=%d';

/**
 * 获取直播间配置
 * @param $room_id
 * @return mixed
 * @throws \Exception
 */
public static function getChatConfig($room_id)
{
    if (isset(static::$roomConfigs[$room_id])) {
        return static::$roomConfigs[$room_id];
    }

    $response = file_get_contents(sprintf(self::CHAT_CONFIG_URL, $room_id));
    $response = json_decode($response, true);

    if (empty($response) || $response['code'] != 0) {
        throw new \Exception("Get chat conf failed, reason: {$response['msg']}");
    }

    static::$roomConfigs[$room_id] = $response['data'];

    return $response['data'];
}

接口返回的内容(省略掉了无关的内容):

{
    "code":0,
    "msg":"ok",
    "message":"ok",
    "data":{
        "refresh_row_factor":0.125,
        "refresh_rate":100,
        "max_delay":5000,
        "port":2243,
        "host":"broadcastlv.chat.bilibili.com",
        "token":"pMF5ippgZMpHIHzTsfKHp9YyqmW3yEfuIuL3pXSMXJ8_9UFN-qSRIPTRxNjpNOr5HQ2-ajI0RcSQkMud2_-lMLoEN92k1glp9fOslshF5SFDqDhlEJRAvUwezoyz72ZdNh-sSqHMPsGOJGMOXZmRNA"
    }
}

认证并加入房间

通过 data 中的 host 和 port 就可以对弹幕服务器发起连接,连接建立后需要发送认证包加入房间。

认证包的内容:

{
  "uid": "0 表示未登录,否则为用户ID",
  "roomid": "房间ID",
  "protover": "协议版本号",
  "platform": "平台",
  "clientver": "客户端版本号",
  "token": "接口返回的 token"
}

认证包的内容就是弹幕协议中携带的数据。

public static function getAuthenticatePacket($room_id, $token = null)
{
    if (empty($token)) {
        $token = static::getChatConfig($room_id)['token'];
    }

    $payload = \json_encode([
        'uid' => 0,
        'roomid' => $room_id,
        'protover' => Packet::PROTOCOL_VERSION,
        'platform' => 'web',
        'token' => $token,
    ]);

    return Packet::pack(Opcode::AUTHENTICATION, $payload);
}

返回的内容:

\000\000\000�\000\000\000\000\000\000\000\000{"uid":0,"roomid":22590309,"protover":2,"platform":"web","token":"pMF5ippgZMpHIHzTsfKHp9YyqmW3yEfuIuL3pXSMXJ8_9UFN-qSRIPTRxNjpNOr5HQ2-ajI0RcSQkMud2_-lMLoEN92k1glp9fOslshF5SFDqDhlEJRAvUwezoyz72ZdNh-sSqHMPsGOJGMOXZmRNA"}

弹幕服务器收到认证包后,会回复我们加入成功的消息,Packet::unpack 后得到消息内容:

array(6) {
  'packet_len' => int(26)
  'header_len' => int(16)
  'protocol_version' => int(2)
  'opcode' => int(8)
  'magic_number' => int(1)
  'payload' => string(10) "{"code":0}"
}

opcode 为 8 表示是服务器发送的心跳包,payload 是一个 JSON 字符串,code 为 0 表示连接成功。

这一步完成之后就可以收到弹幕消息了,但是还差最后一步。

维持在线状态

弹幕服务器要求每隔 30 秒发送一次心跳包,以确定客户端还处于活跃状态。

心跳包没有数据,只需要发送 opcode 为 2 的数据包就可以了。

public static function getHeartBeatPacket()
{
    return Packet::pack(Opcode::CLIENT_HEARTBEAT);
}

考虑到网络传输的因素,心跳包间隔时间一般设置小于 30 秒,防止一些原因导致心跳包没有及时发送。

实现弹幕客户端

可以使用 Workerman、Swoole 甚至 PHP 原生 socket 来实现弹幕客户端,那为啥要用 Workerman 呢?

简单、方便,最重要的是写起来快,不用装扩展也没有原生 socket 那么繁杂,三两下就写完了。

一句话:就是通透

由于篇幅的原因,我会摘取重要的部分来讲,完整的代码可以去 GitHub 获取完整代码。

话不多说,干就完了。

连接弹幕服务器

Worker 进程启动后,通过 AsyncTcpConnection 创建异步 TCP 连接对象。

在 onConnect 回调中发送认证包、开启定时任务,每隔 20 秒发送一次心跳包。

$room_id = 22590309;
/* 获取直播间配置 */
$config = BilibiliBarrage::getChatConfig($room_id);

/* 创建异步 TCP 连接对象 */
$conn = new AsyncTcpConnection("tcp://{$config['host']}:{$config['port']}");

$conn->onConnect = function(TcpConnection $conn) use ($room_id, $config) {
    $packet = BilibiliBarrage::getAuthenticatePacket($room_id, $config['token']);
    /* 发送认证包 */
    $result = $conn->send($packet, true);
    if (!$result) {
        Worker::safeEcho("发送认证包失败\n");
        return;
    }

    /* 开启定时任务 */
    Timer::add(BilibiliBarrage::HEART_BEAT_INTERVAL, function (TcpConnection $conn) {
        /* 发送心跳包 */
        $conn->send(BilibiliBarrage::getHeartBeatPacket(), true);
    }, [$conn]);
};

处理弹幕消息

在 onMessage 回调中,先 unpack 数据,通过 opcode 判断本次消息是做什么的,不同的消息做不同的处理。如果 opcode 为 CMD,需要通过 Packet::parsePayload 解析数据才能得到真正的消息内容。

$conn->onMessage = function($conn, $data) {
    $packet = Packet::unpack($data);
    /* 通过 opcode 判断消息类型 */
    switch ($packet['opcode']) {
        case Opcode::POPULARITY_VALUE:
            Worker::safeEcho(sprintf("人气值: %d\n", Packet::parsePayload($packet['opcode'], $packet['payload'])));
            break;
        case Opcode::CMD:
            /* 解析数据 */
            $payload = Packet::parsePayload($packet['opcode'], $packet['payload']);
            if (empty($payload)) {
                break;
            }

            switch ($payload['cmd']) {
                case 'INTERACT_WORD':
                    Worker::safeEcho("{$payload['data']['uname']} 进入直播间\n");
                    break;
                case 'DANMU_MSG':
                    Worker::safeEcho("{$payload['info'][2][1]}: {$payload['info'][1]}\n");
                    break;
                case 'SEND_GIFT':
                    Worker::safeEcho("{$payload['data']['uname']} {$payload['data']['action']} {$payload['data']['giftName']}\n");
                    break;
                case 'COMBO_SEND':
                    Worker::safeEcho("{$payload['data']['uname']} {$payload['data']['action']} {$payload['data']['gift_name']} [combo]\n");
                    break;
                /* 更多命令查看 \App\CMD.php 文件 */
            }
            break;
        case Opcode::SERVER_HEARTBEAT:
            Worker::safeEcho("加入房间成功\n");
            break;
        default:
            /* 未知的 opcode 可以打印 packet */
            // var_dump($packet);
            break;
    }
};

总结

最后附上一张运行图:

⚠️ 注意!!!本文及源码仅用于学习研究!请勿用于商业或非法目的,否则后果自负。

相关链接:

  • 弹幕姬
  • 获取bilibili直播弹幕的WebSocket协议
  • PHP: pack - Manual
  • 本文源码
博客地址:她和她的猫,欢迎关注。
本文转载于网络 如有侵权请联系删除

相关文章

  • 【Spring Boot实战与进阶】自定义事件及监听

    SpringBoot是很优秀的框架,它的出现简化了新Spring应用的初始搭建以及开发过程,大大减少了代码量,目前已被大多数企业认可和使用。这个专栏将对SpringBoot框架从浅入深,从实战到进阶,不但我们要懂得如何去使用,还要去剖析框架源码,学习其优秀的设计思想。 汇总目录链接:【SpringBoot实战与进阶】学习目录文章目录示例一1、自定义事件2、定义事件监听器3、使用容器中发布事件示例二(注解式,最常用)1、自定义事件2、@EventListener注解的方式监听3、使用容器中发布事件示例三(配置文件)1、自定义事件2、定义事件监听器3、使用容器中发布事件4、application.properties中配置5、控制台输出   这里的自定义事件及监听,其实早在Spring框架就有完善的事件监听机制。Spring的事件为Bean与Bean之间的消息通信提供了支持。当一个Bean处理完任务后,希望另一个Bean知道并能做相应的处理,这时就需要让另一个Bean监听当前Bean的所发送的事件。Spring框架中实现监听事件的流程: (1)自定义事件,继承ApplicationEven

  • 前端学数据结构与算法(四):理解递归及拿力扣链表题目练手

    前言再没对递归了解之前,递归一直是个人的噩梦,对于写递归代码无从下手,但当理解了递归之后,才惊叹到,编程真的是一门艺术。在01世界里,递归是极其重要的一种算法思想,不可能绕的开。这一章我们从调用栈、图解、调试、用递归写链表的方式,再进一步巩固上一章链表的同时,也更进一步理解递归这种算法思想。什么是递归?《盗梦空间》大家应该都看过,那么你可以把递归想象成电影里的梦境,当在这一层没有得到答案时,就进入下一层的梦境,直到在最后一层找到了答案,然后返回到上一层梦境,逐层返回直到现实世界,递归结束。所以递归二字描述的其实是解决问题的两个过程,首先是递,然后是归。而递与归之间的临界点,又可以叫做递归终止条件,意思是我们告诉计算机:行了,别递了,开始归的过程吧您嘞。函数调用栈为了更好的理解递归,函数调用栈这个前提还是得先弄明白了,我们首先来看下这段程序`:functiona(){ b(); console.log('a') } functionb(){ c(); console.log('b') } functionc(){ console.log('

  • 数值分析常见习题解答

    1.已知下列数值表,求符合表值的插值多项式,并给出插值余项的表达式。xix_ixi​000111222yiy_iyi​222111222yi′y_i^{'}yi′​−2-2−2−1-1−1yi′′y_i^{''}yi′′​−10-10−10x_i012y_i212y_i^{'}-2-1y_i^{''}-10解:采用牛顿插值:P_2(x)=f(x_0)+f[x_0,x_1](x-x_0)+f[x_0,x_1,x_2](x-x_0)(x-x_1)​=x^2-2x+2由题目条件可得符合该表的插值多项式可设为:P_5(x)=P_2(x)+(ax^2+bx+c)x(x-1)(x-2)代入以下条件,有: KaTeXparseerror:Unknowncolumnalignment:1atposition28:…\begin{array}{1̲}P…我们可以得到:P_5(x)=4x^5-15x^4+17x^3-5x^2-2x+2R(f)=\frac{f^{(6)}(\xi)}{6!}x^3(x-1)^2(x-2),式中\xi位于x_0,x_

  • Windows 技术篇-电脑秒速关机,注册表修改3个缓冲等待时间

    电脑在关机时为了所有程序可以正常退出,会有一段缓冲等待时间。 比如word的话,如果没有手动保存文档,电脑关机前,他会自动的备份一份存档,下次我们再打开word时会提示要不要恢复就是因为这个。等待了这个时间后,基本程序就备份完了,如果这个时间没有备份完,那就没有备份了。现在的电脑性能越来越好,备份简单文档的话很快就完成了,还有就是重要的文档之类的我们都会手动备份。在这两点基础上,这个等待时间没有什么必要了,我们来把它设置短一点。 当然不能太短,太短可能会出现一些问题,每次都疯狂的强制杀掉程序和服务,下次就有一定可能会出错,这是非常小的概率。打开注册表,详细位置在我的图片里。点击图片查看原图。WaitToKillServiceTimeout等待去强制杀掉服务的时间,我把时间设置为了2秒,双击设置为2000就可以了。 WaitToKillAppTimeout等待去强制杀掉程序进程的时间,我把时间设置为了5秒。 HungAppTimeout等待去强制杀掉停止响应的程序的时间,这个和第二个位置是相同的,我设置的时间是3秒。 注:如果这3个字符串少了哪个,直接右键建一个字符串就可以了,名字相同

  • C++雾中风景番外篇4:GCC升级二三事

    最近将手头上负责的项目代码从GCC4.8.2升级到了GCC8.2。(终于可以使用C++17了,想想后续的开发也是很美好啊~~)不过这个过程之中也遇到了一些稀奇古怪的问题,在这里做一个简单的记录,希望后续有同学遇到类似的问题能作为参考。 1.error:unabletofindstringliteraloperator'operator"这个我感觉是历史的遗留问题了,从C++11开始就不支持字符串字面量后面直接连接变量名,GCC4.8.2应该是没有支持该编译检查,所以后续升级8.2的时候报了类似的错误。听着有些抽象啊,举个栗子:#defineLOG(fmt,...)printf("[%s][%s][%d]:"fmt"\n",__FILE__,__FUNCTION__,\ __LINE__,##__VA_ARGS__)复制上面是一段C++常用的日志宏定义,在宏定义展开的时候,编译器会默认将[%s][%s][%d]:,fmt,"\n"字面量拼接在一起,然后和后面行号等宏定义作为参赛打印出来。#defineLOG(

  • 【AlexeyAB DarkNet框架解析】二,数据结构解析

    按照前面的思路,这一节进入到DarkNet的数据结构解析。Darknet是一个C语言实现的神经网络框架,这就决定了其中大多数保存数据的数据结构都会使用链表这种简单高效的数据结构。基础数据结构为了解析网络配置参数,DarkNet中定义了三个关键的数据结构类型。list类型变量保存所有的网络参数,section类型变量保存的是网络中每一层的网络类型和参数,其中的参数又是使用list类型来表示。kvp键值对类型用来保存解析后的参数变量和参数值。list类型定义在src/list.h中,代码如下://链表上的节点 typedefstructnode{ void*val; structnode*next; structnode*prev; }node; //双向链表 typedefstructlist{ intsize;//list的所有节点个数 node*front;//list的首节点 node*back;//list的普通节点 }list; 复制section类型定义在src/parser.c文件中,代码如下://定义section typedefstruct{ char*type;

  • 常用的几个数组操作方法

    1.取余分割数组varchartArr=[1,2,3,4,5,6,7]; Array.prototype.splitArray=function(num){ varm=this; varcurrData=[]; vardoneData=[]; for(vari=0;i<m.length;i++){ currData.push(m[i]); if(i!=0&&(i+1)%4==0||i==m.length-1){ doneData.push(currData); currData=[]; }; }; if(doneData.length>0){ returndoneData; }else{ returnfalse; }; }; varnewArr=chartArr.splitArray(4);复制2. 数组去重Array.prototype.unique=function(){ vararr=[]; varobj={}; for(vari=0;i<this.length;i++){ if(!obj[this[i]]){ arr.push(this[i])

  • webpack中的loader

    1.loader概述 在实际开发过程中,webpack默认只能打包处理.js后缀名结尾的模块,其他非.js后缀名结尾的模块,webpack默认处理不了,需要调用loader加载器才可以正常打包,否则会报错! loader加载器的作用:协助webpack打包处理特定的文件模块。比如: css-loader可以打包处理.css相关的文件 css-loader可以打包处理.less相关的文件 babel-loader可以打包处理webpack无法处理的高级JS语法  2.loader处理流程 3.打包处理CSS文件   1.运行npmistyle-loader@3.0.0css-loader@5.2.6-D   2.在webpack.gonfig.js的module->rules数组中,添加loader规则如下: module:{//所有第三方文件模块的匹配规则 rule:[//文件后缀名的匹配规则 {test:/\.css$/,use:['style-loader','css-loader']} ] }复制   其中,test表示匹配的文件类型,use表示对应要调用的l

  • 归并排序和快速排序的衍生问题

    前面两篇总结了常见的几种排序算法的主要思想以及C++与python两种方式的实现过程,几种排序算法中比较重要的就是归并排序和快速排序,这两种方法的相同点就是都使用了分治的思想,现在用来解决两个具体问题。 1.分治法   分治法就是将原问题分割成同等结构的子问题,之后将子问题逐一解决后,原问题也就得到了解决。需要注意的是归并排序和快速排序虽然都使用了分治的思想,但它们分别代表了分治算法的两类基本思想。对于归并排序而言,它对“分”这个过程没有做太多操作,只是简单的将数组分为两部分然后递归的进行归并排序,而归并排序的关键是这样分完之后如何将它们归并起来,即merge()操作。   而对于快速排序来说,则是废了很大功夫放在了如何“分”这个问题上,我们是选取了一个标定点,然后使用partition()这个子过程将这个标定点移到了合适的位置,当它移到了合适的位置之后才将整个数组分成了两部分,而这样分完之后,在“合”的时候就不用做过多的考虑了,只需要一步一步递归下去就好了。   下面解决两个直接从归并排序和快速排序中衍生出来的具体问题的。 2.求逆序对数量 问题描述:在数组中的

  • ThinkPhp框架:分页查询

    一、一个条件的查询数据 查询数据自然是先要显示出数据,然后根据条件进行查询数据 (1)显示出表的数据 这个方法我还是写在了HomeController.class控制器文件中 (1.1)写了一个方法shouye() publicfunctionshouye() { $n=M("nation");//数据库中的表 $arr=$n->select();//查询表中的所有数据 $this->assign("shuju",$arr);//将二维数组注入变量 $this->show();//显示数据 } 复制 (1.2)方法写完了之后,下面就是写显示页面了,这个名字和方法名一致 这里我们用表格来显示一下数据 <tablewidth="50%"border="1"cellpadding="0"cellspacing="0"> <tr> <td>代号</td> <td>名称</td> <td>操作</td> </tr><br> <!--注意:这里的循环

  • Python 读取 支付宝账单并存储到 Access 中

    我有一个很多年前自己写的C#+Access的记账程序,用了很多年,现在花钱的机会多了,并且大部分走的支付宝,于是就想把账单从支付宝网站上下载下来,直接写入到Access,这样就很省心了。 记账程序是长这个样子的:   还有报表汇总模块儿:   Access主要表结构如下:   支付宝支付流水下载下来如下:   具体代码如下: table="收支记录表" path="C:\\Users\\user\\Desktop\\tmp\\alipay_record_20190114.xlsx" #test_connect() read_excel(path)复制   #-*-coding:utf-8-*- importpypyodbc importxlrd fromClassDefimport* importCommonUtil importtraceback复制       defread_excel(path): workbook=xlrd.open_workbook(path) sheet=

  • neo4j 双列表匹配

    查询语句: matchp=(nn:Package{packageID:"antlr@@3.4.1"})-[r:Target_To*4..6]->(m:Package) whereall(r_childinrwhereany(ainr_child.targetFrameworkwhereacontains".NETFramework"ora.targetFrameworkin[".NETStandard1.1",".NETStandard1.0"])) andnot(m)-[]->()andall(mminnodes(p)wheremm.isPrerelease="False") RETURNreduce(node_ids=[],nINnodes(p)|node_ids+[n])asresultlimit2000复制  

  • 438. 找到字符串中所有字母异位词

    题目描述: 给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。 字符串只包含小写英文字母,并且字符串 s 和p 的长度都不超过20100。 说明: 字母异位词指字母相同,但排列不同的字符串。不考虑答案输出的顺序。示例 1: 输入:s:"cbaebabacd"p:"abc" 输出:[0,6] 解释:起始索引等于0的子串是"cba",它是"abc"的字母异位词。起始索引等于6的子串是"bac",它是"abc"的字母异位词。 示例2: 输入:s:"abab"p:"ab" 输出:[0,1,2] 解释:起始索引等于0的子串是"ab",它是"ab"的字母异位词。起始索引等于1的子串是"ba",它是"ab"的字母异位词。起始索引等于2的子串是"ab",它是"ab"的字母异位词。   思想:滑动窗口与LeetCode567题几乎一样,只是需要找到一个合法异位词(排列)之后将起始索引加入 res 

  • windows7下的cmd命令之powercfg命令,不常用的

    POWERCFG<命令行选项>描述: 此命令行工具使用户能够控制系统上的 电源设置。参数列表: -LIST,-L  列出当前用户环境中的所有电源方案。             用法:POWERCFG-LIST -QUERY,-Q 显示指定电源方案的内容。             用法:POWERCFG-QUERY<SCHEME_GUID><SUB_GUID>             <SCHEME_GUID> (可选)指定要显示的电源   &nbs

  • JS与CSS在页面加载过程中如何阻塞页面的渲染

    通常浏览器加载并渲染页面包含如下几部分工作: (1)请求HTML资源 (2)解析HTML生成DOM树 (3)遇到JS则去下载,并执行 (4)遇到CSS则去下载,并解析CSS文件生成CSSOM (5)根据DOM树和CSSOM生成渲染树让GPU渲染 对于整个过程中,JS与CSS会对其有如下影响: (1)JS会阻塞DOM树的解析过程,一旦遇到JS脚本,则终止DOM树解析,去下载JS脚本,之后立即执行JS(如果是内嵌JS,则无需下载直接执行),最后继续DOM树解析 (2)CSS并不会阻塞DOM树的解析过程,遇到CSS文件,网络进程会下载CSS文件,不影响DOM树的解析,但会阻塞页面的渲染。(这里DOM树解析!=渲染) (3)CSS虽然不会直接阻塞DOM树的解析过程,但如果在执行JS脚本前,之前的CSS文件还没有加载完成,则会等待其加载完成后才执行JS,所以如果有<link>在<script>之前,那也会有可能由于CSS下载较慢间接阻塞DOM解析过程。 导致以上现象的原因,是由于 (1)CSSOM的解析生成与DOM的解析生成过程没有依赖,互不影响 (2)JS脚本可以控制D

  • deque容器-双端数组

    3.3deque容器 3.3.1deque容器基本概念 功能: 双端数组,可以对头端进行插入删除操作 deque与vector区别: vector对于头部的插入删除效率低,数据量越大,效率越低 deque相对而言,对头部的插入删除速度回比vector快 vector访问元素时的速度会比deque快,这和两者内部实现有关 deque内部工作原理: deque内部有个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据 中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间 deque容器的迭代器也是支持随机访问的 3.3.2deque构造函数 功能描述: deque容器构造 函数原型: deque<T>deqT;//默认构造形式 deque(beg,end);//构造函数将[beg,end)区间中的元素拷贝给本身。 deque(n,elem);//构造函数将n个elem拷贝给本身。 deque(constdeque&deq);//拷贝构造函数 示例: #include<deque> voidprintDeque(co

  • 编钟演绎 - DFS

    编钟演绎 http://go.helloworldroom.com:50080/problem/2719 题目描述 同学们在古典乐器馆见到了编钟,领略了编钟清脆明亮、悠扬动听的音质。谱曲体验更是让同学们跃跃欲试。游戏开始,屏幕上自动生成若干个音符,每个音符都用一个整数表示其音调高低,同学们可以选择保留或舍弃这个音符,最终按音符原有顺序形成自己的曲谱。峰谷交错的曲谱被认为是优美的,计算机会自动合成编钟的音质并播放出来。不满足峰谷交错的曲谱会被系统拒绝。所谓峰谷交错,即除了首尾的音符外,其他所有的音符的音调要么同时比左右两个音低,要么同时比左右两个音高。如下图所示的曲谱计算机就不会认定为优秀,系统将拒绝播放。 面对屏幕上给出的n个音符,计算优美乐谱的最大长度。 输入格式 第一行,包含一个正整数n,表示生成的音符个数。第二行,包含n个整数,依次表示每个音符的强度hi。 输出格式 一行,包含一个数,表示优美乐谱的最大长度 样例数据 input 5 53212复制 output 3复制 样例说明512;312;212都是优美乐谱,最大长度为3。 数据规模 对于20%的数据,n≤10; 对于3

  • 在Android&#160;Studio上进行OpenCV&#160;3.1开发环境配置

    开发环境:     Windows7x64家庭版     AndroidStudio1.5.1(Gradle版本2.8)     JDK1.8.0     Android6.0(API23)     OpenCV3.1.0 AndroidSDK 一、下载OpenCV3.1.0 AndroidSDK  在http://sourceforge.net/projects/opencvlibrary/files/opencv-android/3.1.0/OpenCV-3.1.0-android-sdk.zip/download下载OpenCV3.1.0 AndroidSDK,解压到某个不限制读写权限的目录下。 二、将OpenCV引入AndroidStudio 在AndroidStudio中选择File-->ImportModule,找到OpenCV解压

  • guice的能力简述

    guice这个google出的bean容器框架,ES有用到他。 能干什么 是一个bean容器 能AOP 能力细分与使用方式 以module创建injector。可以看成是一个容器。Module需要自定义且继承自他的AbstractModule。覆写config方法完成装配关系的确定。详细参见这里 绑定顶层接口到具体实现类。bind(TransactionLog.class).to(DatabaseTransactionLog.class);支持bind(A).to(B)然后链式的bind(B).to(C) 支持在构造函数上打上Inject注解标签,用于注入字段 支持自定义注解用于标志装配目标,比如自定义注解Paypal。对于加了PayPal注解的参数,注入PaypalCreditCardProcessor实现,其余的注入GoogleCheckoutProcessor实现。bind(CreditCardProcessor.class).annotatedWith(PayPal.class).to(PaypalCreditCardProcessor.class); 对于加了Named

  • PyQt5-QTableWidget

    1.设计的GUI界面为: 2.对象查看器 3.myWidget.py文件 importsys fromPyQt5.QtWidgetsimport(QApplication,QMainWindow,QLabel,QTableWidgetItem,QAbstractItemView) fromenumimportEnum##枚举类型 fromPyQt5.QtCoreimportpyqtSlot,Qt,QDate fromPyQt5.QtGuiimportQFont,QBrush fromui_QtAppimportUi_MainWindow classCellType(Enum):##各单元格的类型 ctName=1000 ctSex=1001 ctBirth=1002 ctNation=1003 ctScore=1004 ctPartyM=1005 classFieldColNum(Enum):##各字段在表格中的列号 colName=0 colSex=1 colBirth=2 colNation=3 colScore=4 colPartyM=5 classQmyMai

  • 关于MYSQL 和INNODB的逻辑关系图。最好的理解是一点点动手做,观察,记录,思考。

      每隔0.1秒就刷一次MYSQL文件的变化,并闪动标示出来,以观察SQL执行时,MYSQL的处理顺序。 watch-n0.1-dstat/var/lib/mysql/ib_logfile0/var/lib/mysql/ib_logfile1/var/lib/mysql/ibdata1     我理解的执行顺序:(还缺插入缓冲的合并)很粗的框架理解,但折腾了许多天。 硬盘的数据文件mytable.ibd存有mytable这个表id=1的记录,name=123的。(innodb_per_file设置为on) 当UPDATE MYTABLE SET NAME=’ABC’ WHERE ID=1这个语句执行时,系统生成一个LSN是1 INNODB READ THREAD 将这条记录以及词典从硬盘中读到内存的数据页DATA PAGE中,并写到内存中一块UNDO BUFFER中。并对这条记录加锁(如果有索引就是行级锁,否则就是表锁) INNODB WRITE

相关推荐

推荐阅读