wenmo8 发布的文章

本文介绍实时linux方案PREEMPT-RT提升系统实时性的机制之一--中断线程化,以及中断线程优先级配置,希望能为感兴趣的读者提供些许参考。

什么是实时

实时的分类
为什么普通Linux不实时?

常见的RTOS

latency和jitter

一、什么是中断线程化

1. 普通Linux中断处理

中断是一种异步事件处理机制,用于响应硬件请求,它会打断进程的正常调度和执行,然后调用内核中断处理程序来响应设备的请求。

由于中断处理程序会打断其他进程的运行,所以,为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行。如果中断本身要做的事情不多,那么处理起来也不会有太大问题;但如果中断要处理的事情很多,中断服务程序就有可能要运行很长时间。特别是,中断处理程序在响应中断时,还会临时关闭中断。若中断任务繁重,这就会导致上一次中断处理完成之前,其他中断都不能响应,甚至丢失中断。

所以Linux 将中断处理过程分成了两个阶段,也就是上半部和下半部:

  • 上半部
    :快速处理硬件中断,inux不支持中断嵌套,
    它在中断禁止模式下运行
    ,主要处理跟硬件紧密相关的或时间敏感的工作,即硬中断(Interrupt Request,IRQ)。
  • 下半部
    :延迟处理上半部遗留任务,通常以内核线程方式运行,即软中断(SoftIRQ)。

硬中断处理程序(ISR)迅速响应硬件事件,执行最少工作,大部分任务推迟至软中断SoftIRQ或任务队列workqueue。

软中断是一种机制,用于在稍后执行较耗时的中断处理任务。一般以内核线程的方式执行,并且每个 CPU 都对应一个软中断内核线程,名字为 “ksoftirqd/CPU 编号”,如0 号 CPU 对应的软中断内核线程的名字就是 ksoftirqd/0。软中断在软中断上下文运行,执行时机有2个,
一个是硬件中断返回后立即执行,这时和硬件中断一样,完全抢占进程上下文,不允许被调度和抢占,其执行时间为不超过2ms,如果执行的软中断超过了2ms会将软中断延迟到软中断线程 “ksoftirqd/CPU 编号”中执行

系统中软硬件中断通过proc 文件系统来查看:

  • /proc/softirqs 提供了软中断的运行情况;

  • /proc/interrupts 提供了硬中断的运行情况。

2. 实时性的不足

传统的中断处理方式虽然提升了系统的吞吐量,但从实时任务响应实时性方面来看存在以下不足:

  • 中断禁用
    :inux不支持中断嵌套,在硬中断处理过程中,其他中断会被屏蔽,增加了延迟。
  • 中断串行处理
    :inux不支持中断嵌套,同一cpu上的外设中断处理串行执行,这在非紧急外设中断多的情况下导致紧急外设中断处理的不确定性。
  • 不可预知的延迟
    :不同的驱动程序,硬中断处理程序执行需要的时间不同,不可预知,可能导致高优先级任务被中断抢占延迟执行,影响任务的实时性能。
  • 软中断的不确定性
    :软中断在硬中断退出后前期处理是完全抢占进程上下文的,它的处理时间和调度顺序也可能影响到实时任务的执行。

image-20241216221938065

这些不足使得普通linux中断处理方式难以满足高实时性要求的应用场景。

3. 中断线程化

为了解决上述问题,PREEMPT-RT补丁引入了中断线程化机制,于2009年合入linux主线版本2.6.30。顾名思义就是
将原来硬件中断上下文的中断处理的部分也通过一个内核线程来处理
,启用PREEMPT-RT后(系统抢占配置为Full Real Time Preemption)默认将除代码中明确指明非中断线程化的中断和系统硬件timer中断外的硬件中断均通过中断线程来处理,
中断线程默认是一个实时线程,调度类为SCHED_FIFO,实时优先级为50
。同时将SoftIRQ全部放到
ksoftirqd/CPU
线程中执行,
ksoftirqd/CPU
为普通非实时任务不变。通过
ps
命令可以看到中断线程。

ubuntu@work-host:~$ ps -ef | grep irq
root          14       2  0 12月10 ?      00:02:44 [ksoftirqd/0]
root          19       2  0 12月10 ?      00:00:08 [irq_work/0]
root          25       2  0 12月10 ?      00:00:00 [irq_work/1]
root          28       2  0 12月10 ?      00:00:00 [ksoftirqd/1]
root          33       2  0 12月10 ?      00:00:04 [irq_work/2]
root          36       2  0 12月10 ?      00:02:47 [ksoftirqd/2]
root          41       2  0 12月10 ?      00:00:01 [irq_work/3]
root          44       2  0 12月10 ?      00:03:09 [ksoftirqd/3]
root          49       2  0 12月10 ?      00:00:04 [irq_work/4]
root          52       2  0 12月10 ?      00:02:42 [ksoftirqd/4]
root          57       2  0 12月10 ?      00:00:02 [irq_work/5]
root          60       2  0 12月10 ?      00:02:28 [ksoftirqd/5]
root         121       2  0 12月10 ?      00:00:00 [irq/9-acpi]
root         136       2  0 12月10 ?      00:00:00 [irq/120-PCIe PM]
root         137       2  0 12月10 ?      00:00:00 [irq/121-PCIe PM]
root         138       2  0 12月10 ?      00:00:00 [irq/121-aerdrv]
root         139       2  0 12月10 ?      00:00:00 [irq/121-s-aerdr]
root         140       2  0 12月10 ?      00:00:00 [irq/122-PCIe PM]
root         141       2  0 12月10 ?      00:00:00 [irq/122-aerdrv]
root         142       2  0 12月10 ?      00:00:00 [irq/122-s-aerdr]
root         143       2  0 12月10 ?      00:00:00 [irq/123-PCIe PM]
root         144       2  0 12月10 ?      00:00:00 [irq/123-aerdrv]
root         145       2  0 12月10 ?      00:00:00 [irq/123-s-aerdr]
root         149       2  0 12月10 ?      00:00:00 [vfio-irqfd-clea]
root         150       2  0 12月10 ?      00:00:00 [irq/8-rtc0]
root         229       2  0 12月10 ?      00:13:52 [irq/124-xhci_hc]
root         230       2  0 12月10 ?      00:00:00 [irq/16-i801_smb]
root         281       2  0 12月10 ?      00:00:00 [irq/126-ahci[00]
root         746       2  0 12月10 ?      00:00:00 [irq/142-mei_me]
root        1089       2  0 12月10 ?      00:00:05 [irq/143-i915]
root        1105       2  0 12月10 ?      00:00:00 [irq/144-snd_hda]
root        1465       1  0 12月10 ?      00:01:24 /usr/sbin/irqbalance --foreground
root        1731       2  0 12月10 ?      00:00:20 [irq/125-eno1]
root        1734       2  0 12月10 ?      00:00:00 [irq/127-enp2s0]
root        1735       2  0 12月10 ?      00:00:04 [irq/128-enp2s0-]
....

中断线程化通过以下方式提升了Linux系统的实时性:

  • 减少中断禁用时间
    :由原本整个硬件中断处理过程都需要屏蔽全局中断,变成仅响应中断,唤醒中断线程部分会屏蔽全局中断,这部分只执行最少量的处理,减少了中断禁用的时间,降低了整体延迟。
  • 优先级管理
    :中断处理程序作为内核线程,基本不会屏蔽全局中断,并且不同优先级的外设中断线程,可以被赋予不同的优先级,从而避免低优先级的外设中断阻塞高优先级外设、任务的执行。
  • 可抢占性
    :中断线程作为可调度的内核线程,可以被高优先级任务抢占,使系统响应更加灵活和实时。
  • 可预期性增强
    :中断处理时间变得更加可控和可预见,提高了系统的实时性能。
    -
    系统吞吐量降低
    :中断线程化后,CPU上下文切换增多,那 CPU 的处理效率就会打一定折扣。
    image-20241216221938065

通过这些改进,PREEMPT-RT的中断线程化机制有效地解决了传统中断处理方式在实时性上的不足,使Linux系统能够更好地满足高实时性应用的需求。

二、中断线程优先级配置

所有外设的中断线程默认一个实时线程,调度类为SCHED_FIFO,实时优先级为50,对于实时性要求高的外设应该设置更高的优先级,如何才能修改中断线程的优先级?方式有两种,第一种最直接的办法,驱动代码中添加代码修改,但这样不灵活。另外一种是通过chrt工具来修改。

使用chrt修改中断线程优先级

例如,要将PID为1234的线程的实时优先级设置为50,可以执行以下命令:

chrt -f -p 50 1234

这里,
-f
表示设置实时优先级,
-p
表示操作对象是进程。同样,只有root用户才能修改线程的实时优先级。

注意:在使用这些命令时,请确保已正确指定PID,以免对其他进程造成不必要的影响。
但直接使用chrt还是太原始,下面我给大家推荐一个工具--rtirq。

使用rtirq修改中断线程优先级

安装rtirq

如果你使用的是debian衍生发行版,还可以通过
rtirq
这个工具来修改中断线程优先级,直接从github拉取

git clone https://github.com/rncbc/rtirq

或者通过apt软件包管理器安装

sudo apt install rtirq-init

注意
:rtirq工具依赖完整功能的ps和chrt,所以在一些使用busybox嵌入式系统中由于ps功能简单,rtirq运行不了。

rtirq使用说明

我们来看看
rtirq-init
是如何工作的,
rtirq
是一个脚本,位于
/etc/init.d/rtirq
,位于
/etc/init.d
目录下的的脚本系统启动后会自动执行,首先读取解析配置文件,然后根据配置文件来修改中断线程的优先级和调度策,最终执行的也是
chrt
命令,只不过它通过配置文件的方式提供了更多的灵活性配置,你可以自行分析脚本源码。
脚本
/etc/init.d/rtirq
接收的参数有
start

stop

reset

restart
(或)
force-reload

status

rtirq脚本执行时使用的配置文件依次为为
/etc/sysconfig/rtirq

/etc/default/rtirq

/etc/rtirq.conf
,前一个存在后面的就不生效。其中
/etc/default/rtirq
是安装后自动生成的模板文件,你可以直接修改它或者基于它重新新建一个。

rtirq配置

/etc/default/rtirq
内容如下:

# 我们要修改的IRQ线程服务名称,按下面的优先级及步长依次配置
# (从优先级高到低,以空格分隔列表)。
# RTIRQ_NAME_LIST="rtc snd usb i8042" #旧版本
RTIRQ_NAME_LIST="snd usb i8042"

# 最高的优先级。
RTIRQ_PRIO_HIGH=90

# 优先级递减步长。
RTIRQ_PRIO_DECR=5

# 最低的优先级。
RTIRQ_PRIO_LOW=51

# 是否将所有 IRQ 线程重置为 SCHED_OTHER。
RTIRQ_RESET_ALL=0

# 当内核支持时,
# 哪些服务不应被线程化(以空格分隔列表)。
RTIRQ_NON_THREADED="rtc snd"

# 将被强制到最高实时优先级范围内的进程名(99-91)(从高到低,以空格分隔列表)
# RTIRQ_HIGH_LIST="timer"
  • RTIRQ_NAME_LIST表示我们要修改的中断线程集合,按优先级从RTIRQ_PRIO_HIGH到RTIRQ_PRIO_LOW高顺序排列,示例中的是snd(90)>usb(85)>i8042(80);
  • RTIRQ_PRIO_HIGH表示第一个RTIRQ_NAME_LIST中第一个中断线程的优先级;
  • RTIRQ_PRIO_DECR表示RTIRQ_NAME_LIST中的中断线程优先级从高到低的步长;
  • RTIRQ_PRIO_LOW表示最低优先级;
  • RTIRQ_RESET_ALL是否将RTIRQ_NAME_LIST外的所有 IRQ 线程重置为 SCHED_OTHER
    如果我们不知道 IRQ 线程的名称,可以通过执行/etc/init.d/rtirq status来获取,同时能看到系统中中断线程的默认优先级:
ubuntu@work-host:~$ /etc/init.d/rtirq status
    PID CLS RTPRIO  NI PRI %CPU STAT COMMAND
   1105 FF      90   - 130  0.0 S    irq/144-snd_hda
    229 FF      85   - 125  0.1 S    irq/124-xhci_hc
    121 FF      50   -  90  0.0 S    irq/9-acpi
    136 FF      50   -  90  0.0 S    irq/120-PCIe PM
    137 FF      50   -  90  0.0 S    irq/121-PCIe PM
    138 FF      50   -  90  0.0 S    irq/121-aerdrv
    150 FF      50   -  90  0.0 S    irq/8-rtc0
    230 FF      50   -  90  0.0 S    irq/16-i801_smb
    281 FF      50   -  90  0.0 S    irq/126-ahci[00
    746 FF      50   -  90  0.0 S    irq/142-mei_me
   1089 FF      50   -  90  0.0 S    irq/143-i915
   1731 FF      50   -  90  0.0 S    irq/125-eno1
....
   1757 FF      50   -  90  0.0 S    irq/141-enp4s0-
     14 RR       1   -  41  0.0 S    ksoftirqd/0
     28 TS       -   0  19  0.0 S    ksoftirqd/1
     36 TS       -   0  19  0.0 S    ksoftirqd/2
     44 TS       -   0  19  0.0 S    ksoftirqd/3
     52 TS       -   0  19  0.0 S    ksoftirqd/4
     60 TS       -   0  19  0.0 S    ksoftirqd/5
....

示例

  1. 先将所有线程优先级配置为0,配置文件如下:
....
# Whether to reset all IRQ threads to SCHED_OTHER.
RTIRQ_RESET_ALL=1
...

执行:

ubuntu@work-host:~$ sudo /etc/init.d/rtirq reset
ubuntu@work-host:~$ sudo /etc/init.d/rtirq status

    PID CLS RTPRIO  NI PRI %CPU STAT COMMAND
     14 TS       -   0  19  0.0 S    ksoftirqd/0
     28 TS       -   0  19  0.0 S    ksoftirqd/1
     36 TS       -   0  19  0.0 S    ksoftirqd/2
     44 TS       -   0  19  0.0 S    ksoftirqd/3
     52 TS       -   0  19  0.0 S    ksoftirqd/4
     60 TS       -   0  19  0.0 S    ksoftirqd/5
    121 TS       -   0  19  0.0 S    irq/9-acpi
    137 TS       -   0  19  0.0 S    irq/120-PCIe PM
    138 TS       -   0  19  0.0 S    irq/121-PCIe PM
    139 TS       -   0  19  0.0 S    irq/121-aerdrv
    140 TS       -   0  19  0.0 S    irq/121-s-aerdr
    141 TS       -   0  19  0.0 S    irq/122-PCIe PM
    142 TS       -   0  19  0.0 S    irq/122-aerdrv
    143 TS       -   0  19  0.0 S    irq/122-s-aerdr
    144 TS       -   0  19  0.0 S    irq/123-PCIe PM
    145 TS       -   0  19  0.0 S    irq/123-aerdrv
    146 TS       -   0  19  0.0 S    irq/123-s-aerdr
    151 TS       -   0  19  0.0 S    irq/8-rtc0
    251 TS       -   0  19  0.0 S    irq/125-xhci_hc
    252 TS       -   0  19  0.0 S    irq/16-i801_smb
    299 TS       -   0  19  0.0 S    irq/126-ahci[00
    776 TS       -   0  19  0.0 S    irq/142-mei_me
   1121 TS       -   0  19  0.0 S    irq/143-i915
   1136 TS       -   0  19  0.0 S    irq/144-snd_hda
   1798 TS       -   0  19  0.0 S    irq/124-eno1
   1799 TS       -   0  19  0.0 S    irq/127-enp2s0
   1800 TS       -   0  19  0.0 S    irq/128-enp2s0-
   1801 TS       -   0  19  0.0 S    irq/129-enp2s0-
   1802 TS       -   0  19  0.0 S    irq/130-enp2s0-
   1803 TS       -   0  19  0.0 S    irq/131-enp2s0-
   1808 TS       -   0  19  0.0 S    irq/132-enp3s0
   1809 TS       -   0  19  0.0 S    irq/133-enp3s0-
   1810 TS       -   0  19  0.0 S    irq/134-enp3s0-
   1811 TS       -   0  19  0.0 S    irq/135-enp3s0-
   1812 TS       -   0  19  0.0 S    irq/136-enp3s0-
   1822 TS       -   0  19  0.0 S    irq/137-enp4s0
   1823 TS       -   0  19  0.0 S    irq/138-enp4s0-
   1824 TS       -   0  19  0.0 S    irq/139-enp4s0-
   1825 TS       -   0  19  0.0 S    irq/140-enp4s0-
   1826 TS       -   0  19  0.0 S    irq/141-enp4s0-
  1. 修改rtc0、enp4s0的中断线程优先级,配置文件如下:
# IRQ thread service names
# (space separated list, from higher to lower priority).
# RTIRQ_NAME_LIST="rtc snd usb i8042" # old
RTIRQ_NAME_LIST="rtc enp4s0"

# Highest priority.
RTIRQ_PRIO_HIGH=90

# Priority decrease step.
RTIRQ_PRIO_DECR=5

# Lowest priority.
RTIRQ_PRIO_LOW=51

# Whether to reset all IRQ threads to SCHED_OTHER.
RTIRQ_RESET_ALL=0

# On kernel configurations that support it,
# which services should be NOT threaded
# (space separated list).
#RTIRQ_NON_THREADED="rtc snd"

# Process names which will be forced to the
# highest realtime priority range (99-91)
# (space separated list, from highest to lower priority).
# RTIRQ_HIGH_LIST="timer"

执行

ubuntu@work-host:~$ sudo /etc/init.d/rtirq start
Setting IRQ priorities: stop [rtc] irq=8 pid=151: OK.
Setting IRQ priorities: stop [enp4s0] irq=137 pid=1822: OK.
Setting IRQ priorities: stop [enp4s0] irq=138 pid=1823: OK.
Setting IRQ priorities: stop [enp4s0] irq=139 pid=1824: OK.
Setting IRQ priorities: stop [enp4s0] irq=140 pid=1825: OK.
Setting IRQ priorities: stop [enp4s0] irq=141 pid=1826: OK.
ubuntu@work-host:~$ sudo /etc/init.d/rtirq status

    PID CLS RTPRIO  NI PRI %CPU STAT COMMAND
    151 FF      90   - 130  0.0 S    irq/8-rtc0
   1822 FF      85   - 125  0.0 S    irq/137-enp4s0
   1823 FF      84   - 124  0.0 S    irq/138-enp4s0-
   1824 FF      83   - 123  0.0 S    irq/139-enp4s0-
   1825 FF      82   - 122  0.0 S    irq/140-enp4s0-
   1826 FF      81   - 121  0.0 S    irq/141-enp4s0-
     14 TS       -   0  19  0.0 S    ksoftirqd/0
     28 TS       -   0  19  0.0 S    ksoftirqd/1
     36 TS       -   0  19  0.0 S    ksoftirqd/2
     44 TS       -   0  19  0.0 S    ksoftirqd/3
     52 TS       -   0  19  0.0 S    ksoftirqd/4
     60 TS       -   0  19  0.0 S    ksoftirqd/5
    121 TS       -   0  19  0.0 S    irq/9-acpi
    137 TS       -   0  19  0.0 S    irq/120-PCIe PM
    138 TS       -   0  19  0.0 S    irq/121-PCIe PM
    139 TS       -   0  19  0.0 S    irq/121-aerdrv
    140 TS       -   0  19  0.0 S    irq/121-s-aerdr
    141 TS       -   0  19  0.0 S    irq/122-PCIe PM
    142 TS       -   0  19  0.0 S    irq/122-aerdrv
    143 TS       -   0  19  0.0 S    irq/122-s-aerdr
    144 TS       -   0  19  0.0 S    irq/123-PCIe PM
    145 TS       -   0  19  0.0 S    irq/123-aerdrv
    146 TS       -   0  19  0.0 S    irq/123-s-aerdr
    251 TS       -   0  19  0.0 S    irq/125-xhci_hc
    252 TS       -   0  19  0.0 S    irq/16-i801_smb
    299 TS       -   0  19  0.0 S    irq/126-ahci[00
    776 TS       -   0  19  0.0 S    irq/142-mei_me
   1121 TS       -   0  19  0.0 S    irq/143-i915
   1136 TS       -   0  19  0.0 S    irq/144-snd_hda
   1798 TS       -   0  19  0.0 S    irq/124-eno1
   1799 TS       -   0  19  0.0 S    irq/127-enp2s0
   1800 TS       -   0  19  0.0 S    irq/128-enp2s0-
   1801 TS       -   0  19  0.0 S    irq/129-enp2s0-
   1802 TS       -   0  19  0.0 S    irq/130-enp2s0-
   1803 TS       -   0  19  0.0 S    irq/131-enp2s0-
   1808 TS       -   0  19  0.0 S    irq/132-enp3s0
   1809 TS       -   0  19  0.0 S    irq/133-enp3s0-
   1810 TS       -   0  19  0.0 S    irq/134-enp3s0-
   1811 TS       -   0  19  0.0 S    irq/135-enp3s0-
   1812 TS       -   0  19  0.0 S    irq/136-enp3s0-

可以看到,
irq/8-rtc0
实时优先级为90,
irq/137-enp4s0
实时优先级为85,修改成功。

三、总结

本文主要介绍了中断线程化这一提高linux系统实时性的机制。通过将外设中断处理从屏蔽全局中断的硬件中断上下转移到线程上下文程执行,可以更好地控制中断处理的优先级和响应时间,但牺牲了部分系统的吞吐量。此外,文章还详细讨论了如何配置不同外设的中断线程优先级,以确保关键任务的实时性。这些配置方法对系统设计者在规划和优化实时应用的优先级时具有重要意义。

什么是 useSWR ?

听名字我们都知道是一个 React 的 hooks,SWR 是
stale-while-revalidate
的缩写, stale 的意思是陈旧的, revalidate 的意思是重新验证/使重新生效, 合起来的意识可以理解成 在重新验证的过程中先使用陈旧的,在http 请求中意味着先使用已过期的数据缓存,同时请求新的数据去刷新缓存。

这在 http 请求中
Cache-Control
响应头中已经实现,比如:

Cache-Control: max-age=60, stale-while-revalidate=3600

这意味着在缓存过期时间为60秒,当缓存过期时,你请求了该接口,并且在缓存过期后的3600内,会先使用原来过期的缓存作为结果返回,同时会请求服务器去刷新缓存。

示例:

未使用 swr 的情况,缓存过期后直接重放304协商缓存请求

使用 swr 的情况,缓存过期后直接返回200过期的缓存数据,再进行304协商缓存请求

但通过 nginx 等网关层来实现 swr ,没法做到接口缓存的精确控制,并且即使
revalidate
后的
fresh
数据返回了,也没法让页面重新渲染,只能等待下次接口请求。

useSWR
直接在前端代码层实现了http请求SWR缓存的功能。

使用方式

在传统模式下,我们会这样写一个数据请求, 需要通过定义多个状态来管理数据请求, 并在副作用中进行指令式的接口调用。

import { useEffect, useState } from "react";
import Users from "./Users";

export default function App() {
  const [users, setUsers] = useState([]);
  const [isLoading, setLoading] = useState(false);

  const getUsers = () => {
    setLoading(true);
    fetch("/api/getUsers")
      .then((res) => res.json())
      .then((data) => {
         setUsers(data);
      })
      .finally(() => {
         setLoading(false)
      })
  }

  useEffect(() => {
    getUsers();
  }, []);


  return (
    <div>
      {isLoading && <h2>loading... </h2>}
      <UserList users={users} />
    </div>
  );
}

使用 useSWR 后, 我们只需要告诉 SWR 这个请求的唯一 key, 与如何处理该请求的
fetcher
方法,在组件挂载后会自动进行请求

import useSWR from "swr";
import Users from "./Users";

const fetcher = (...args) => fetch(...args).then((res) => res.json())

export default function App() {
  const { data: users, isLoading, mutate } = useSWR('/api/getUsers', fetcher)

  return (
    <div>
      {isLoading && <h2>loading... </h2>}
      <UserList users={users} />
    </div>
  );
}

useSWR的入参

  • key
    : 请求的唯一key,可以为字符串、函数、数组、对象等
  • fetcher
    :(可选)一个请求数据的 Promise 返回函数
  • options
    :(可选)该 SWR hook 的选项对象

key 会作为入参传递给
fetcher
函数, 一般来说可以是请求的URL作为key。可以根据场景自定义 key 的格式,比如我有额外请求参数,那么就把 key 定义成一个数组
['/api/getUsers', { pageNum: 1 }]
, SWR会在内部自动序列化 key 值,以进行缓存匹配。

useSWR的返回

  • data
    : 通过
    fetcher
    处理后的请求结果, 未返回前为
    undefined

  • error
    :
    fetcher
    抛出的错误

  • isLoading
    : 是否有一个正在进行中的请求且当前没有“已加载的数据“。

  • isValidating
    : 是否有请求或重新验证加载

  • mutate(data?, options?)
    : 更改缓存数据的函数

核心

全局缓存机制

我们每次使用 SWR 时都有用到 key,这将作为唯一标识将结果存入全局缓存中,这种
默认缓存
的行为其实非常有用。

例如获取用户列表在我们产品中是一个非常频繁的请求,细分下来用户列表都会有很多个接口

我们在写需求时,可能不知道这个接口数据有没有往 redux 中存过,并且往 redux 中放数据是个相对麻烦的操作有管理成本,那么大多数人的做法就是那有地方用,我就重新请求一遍。

例如一个模态框里存在用户列表,每次打开都要请求一次 (带远程搜索),用户每次都需要等待,当然你也可以把用户列表状态提升到模态框外面,但对应的就会有取舍,外部父组件其实根本不关心用户列表状态。

请求状态区分

当第一次请求,也就是没有找到对应 key 的缓存时,那么就会立即发起请求,
isLoading

isValidating
都为 true。

当第二次请求时,有缓存,那么先拿缓存数据渲染,再进行请求,
isValidating
为 true。

也就是说只要正在请求中,就是
isValidating
, 无缓存数据且正在请求时才为
isLoading
状态。

对应的状态图:

file

以上的案例中都是 key 为固定值的情况,但更多场景下 key 值会由于请求参数的变动而变动。

如一个搜索用户的 key 这样定义

const [search, setSearch] = useState('');
const { data } = useSWR(['/api/users', search], fetcher)

每次输入都会导致 key 变化,key 变化默认就会重新请求接口,但其实 key 变化了也就代表了数据不可信了,需要里面重置数据,因此
data
会被立即重置为
undfined
。如果新的 key 已经有缓存值,那么也会先拿缓存值进行渲染。

那么其实我们几乎什么额外代码也没加,就实现了一个自带数据缓存的用户搜索功能。

对应的key变化时的状态图:

file

假如我们偏要保留 key 变化前的数据先展示呢?因为我们还是会看到短暂的
no-data

我们主要在第三个参数 options 中加入配置项
keepPreviousData
即可实现

file

实现效果与我们 gitlab 搜索分支时其实是一致的

key变化且保留数据的状态图:

file

联动请求与手动触发

很多情况下接口请求都依赖于另外一个接口请求的结果,或者在某种情况下才发起请求。

首先如何让组件挂载时不进行请求,有三种方法

配置项实现

设置
options
参数
revalidateOnMount
, 这种方法如果已有缓存数据,仍然会拿缓存数据渲染

依赖模式

给定 key 时返回
falsy
值 或者 提供函数并抛出错误

const { data } = useSWR(isMounted ? '/api/users' : null, fetcher)

const { data } = useSWR(() => isMounted ? '/api/users' : null, fetcher)

// 抛出错误
const { data: userInfo } = useSWR('/api/userInfo')
const { data } = useSWR(() => '/api/users?uid=' + userInfo.id, fetcher)

那我们实现一个业务场景:数据源-数据库-数据表的联动请求

我们几乎以一种自动化的方式实现了联动请求。

以下是代码示例:

const DependenceDataSource = () => {
    const [form] = Form.useForm();
    const dataSourceId = Form.useWatch("dataSourceId", form);
    const dbId = Form.useWatch("dbId", form);

    const { data: dataSourceList = [], isValidating: isDataSourceFetching } =
        useSWR({ url: "/getDataSource" }, dataSourceFetcher);
        
    const { data: dbList = [], isValidating: isDatabaseFetching } = useSWR(
        () =>
            dataSourceId
                ? { url: "/getDatabase", params: { dataSourceId } }
                : null,
        databaseFetcher
    );

    const { data: tableList = [], isValidating: isTableFetching } = useSWR(
        () =>
            dataSourceId && dbId
                ? { url: "/getTable", params: { dataSourceId, dbId } }
                : null,
        tableFetcher
    );

    return (
        <Form
            form={form}
            style={{width: 400}}
            layout="vertical"
            onValuesChange={(changedValue) => {
                if ("dataSourceId" in changedValue) {
                    form.resetFields(["dbId", "tableId"]);
                }
                if ("dbId" in changedValue) {
                    form.resetFields(["tableId"]);
                }
            }}
        >
            <Form.Item name="dataSourceId" label="数据源">
                <Select
                    placeholder="请选择数据源"
                    options={dataSourceList}
                    loading={isDataSourceFetching}
                    allowClear
                />
            </Form.Item>
            <Form.Item name="dbId" label="数据库">
                <Select
                    placeholder="请选择数据库"
                    options={dbList}
                    loading={isDatabaseFetching}
                    allowClear
                />
            </Form.Item>
            <Form.Item name="tableId" label="数据表">
                <Select
                    placeholder="请选择数据表"
                    options={tableList}
                    loading={isTableFetching}
                    allowClear
                />
            </Form.Item>
        </Form>
    );
};

采用手动挡模式

使用上面这种方法利用了 key 变化会自动
revalidate
数据的机制实现了联动,但是有个非常大的
弊端
,你需要把 key 中所有的依赖参数都提取为
state
使组件能够重新 render 以进行
revalidate
。有点强制你使用受控模式的感觉,这会造成性能问题。

所以我们需要利用
mutate
进行手动请求,
mutate(key?, data, options)

你可以直接从 swr 全局引入
mutate
方法,也可以使用 hooks 返回的
mutate
方法。

区别:

  • 全局
    mutate
    需要额外提供 key
  • hooks 内
    mutate
    直接绑定了key
// 全局使用
import { mutate } from "swr"
function App() {
  mutate(key, data, options)
}

// hook使用
const UsersMutate = () => {
    const { data, mutate } = useSWR({ url: "/getNewUsers" }, fetcher, {
        revalidateOnFocus: false,
        dedupingInterval: 0,
        revalidateOnMount: false
    });

    return (
        <div>
            <Input.Search
                onSearch={(value) => {
                    mutate([{ id: 3, name: "user_" + value }]);
                }}
            />
            <List style={{ width: 300 }}>
                {data?.map((user) => (
                    <List.Item key={user.id}>{user.name}</List.Item>
                ))}
            </List>
        </div>
    );
}

mutate
后会立马使用传入的
data
更新缓存,然后会再次进行一次
revalidate
数据刷新

使用全局
mutate
传入 key
{ url: "/getNewUsers" }
后能够实现一样的效果,并且使用全局
mutate

传入的 key 为函数时,你可以批量清除缓存。注意: mutate 中 key 传入函数表示
过滤函数
,与 useSWR 中传入 key 函数意义不同。

mutate(
    (key) => typeof key === 'object' && key.api === getUserAPI && key.params.search !== '',
    undefined,
  {
    revalidate: false
  }
);

但是,我们可以注意到现在传入的
key
是不带有请求参数的,hooks中
mutate
也无法修改绑定的
key
值,那么怎么携带请求参数呢?

useSWRMutation

useSWRMutation
为一种手动模式的 SWR,只能通过返回的
trigger
方法进行数据更新。

这意味着:

  1. 它不会自动使用缓存数据
  2. 它不会自动写入缓存(可以通过配置修改默认行为)
  3. 不会在组件挂载时或者 key 变化时自动请求数据

它的函数返回稍有不同:

const { data, isMutating, trigger, reset, error } = useSWRMutation(
    key,
    fetcher, // fetcher(key, { arg })
    options
);

trigger('xxx')

useSWRMutation

fetcher
函数额外可以传递一个
arg
参数, 在
trigger
可以中传递该参数,那么我们再来实现
2.1依赖模式
中的依赖联动请求。

  1. 定义三个 fetcher, 接收参数, 这里参数不晓得为啥一定设计成
    { arg }
    形式
const dataSourceFetcher = (key) => {
    return new Promise<any[]>((resolve) => {
        request(key).then((res) => resolve(res))
    });
};

const databaseFetcher = (key, { arg }: { arg: DatabaseParams }) => {
    return new Promise<any[]>((resolve) => {
        const { dataSourceId } = arg;
        if (!dataSourceId) return resolve([])
        request(key, { dataSourceId }).then((res) => resolve(res))
    });
};

const tableFetcher = (key, { arg }: { arg: TableParams }) => {
    return new Promise<any[]>((resolve) => {
        const { dataSourceId, dbId } = arg;
        if (!dataSourceId || !dbId) return resolve([])
        request(key, { dataSourceId, dbId }).then((res) => resolve(res))
    });
};
  1. 定义 hooks
const { data: dataSourceList = [], isValidating: isDataSourceFetching } =
    useSWR({ url: "/getDataSource" }, dataSourceFetcher);

const { data: dbList = [], isMutating: isDatabaseFetching, trigger: getDatabase, reset: clearDatabase } = useSWRMutation(
    { url: "/getDatabase" },
    databaseFetcher,
);

const { data: tableList = [], isMutating: isTableFetching, trigger: getTable, reset: clearTable } = useSWRMutation(
    { url: "/getTable" },
    tableFetcher
);
  1. 手动触发
<Form
    onValuesChange={(changedValue) => {
        if ("dataSourceId" in changedValue) {
            form.resetFields(["dbId", "tableId"]);
            clearDatabase();
            clearTable();
            getDatabase({ dataSourceId: changedValue.dataSourceId });
        }
        if ("dbId" in changedValue) {
            form.resetFields(["tableId"]);
            clearTable();
            getTable({
                dataSourceId: form.getFieldValue("dataSourceId"),
                dbId: changedValue.dbId,
            });
        }
    }}
>
    // FormItem略
</Form>

无缓存写入

file

但是使用
useSWRMutation
这种方式,如果库表还带有远程数据搜索,就没法用到缓存特性了。

性能优化

useSWR 在设计的时候充分考虑了性能问题

  • 自带节流
    当我们短时间内多次调用同一个接口时,只会触发一次请求。如同时渲染多个用户组件,会触发
    revalidate
    机制,但实际只会触发一次。这个时间节流时间由
    dedupingInterval
    配置控制,默认为
    2000ms

A Question, 如何实现防抖呢?无可用配置项

  • revalidate

    freshData

    staleData
    间进行的是深比较,避免不必要渲染, 详见
    dequal

  • 依赖收集
    如果没有消费
    hooks
    返回的状态,则状态变化不会导致重新渲染

const { data } = useSWR('xxx', fetcher);

// 仅在data变化时render, isValidating, isLoading由于没有引入及时变化也不会触发渲染

依赖收集的实现很巧妙

  1. 定义个ref进行依赖收集, 默认没有任何依赖
    file

  2. 通过
    get
    实现访问后添加
    file

  3. 由于
    state
    改变必定会导致渲染,所以这些状态全部由
    useSyncExternalStore
    管理
    file

  4. 只有在不相等时才会触发渲染,如果不在stateDependencies收集中,则直接
    file

总结

useSWR能够极大的提升用户体验,但在实际使用时,可能仍需留点小心思结合业务来看是否要使用缓存特性,如某些提交业务场景下对库表的实时性很高,这时就该考虑是否要有useSWR了。

再者,数栈产品中在实际开发中很少会对业务数据进行hooks封装,如用户列表可以封装成
useUserList
,表格使用
useList
等。感觉更多是开发习惯的原因,觉得以后自己可能也不会复用不会做过多的封装,指令式编程一把梭。

最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

原文地址:
Jetpack Compose学习(15)——Pager组件的使用(对标ViewPager)-Stars-One的杂货小窝

从名字可以看出,Pager这个就是ViewPager的替代产物

在Jetpack Compose里的,Pager根据方向,主要分为2个组件:
VerticalPager
HorizontalPager

这2个,一个是默认占满高度,一个是默认占满宽度,可以自行通过Modifier来修改尺寸

基本使用

val pagerState = rememberPagerState(pageCount = {
	2
})
HorizontalPager(state = pagerState){pageIndex->
	//这里写你的页面内容,根据下标自动切换不同页面
	when (pageIndex) {
        0 -> {
            Box(modifier=Modifier) {             
            }
        }
        else -> {
            Box(modifier=Modifier) {
            }
        }
    }
}

属性讲解

这里Pager的属性有些多,我挑些常用的属性来进行说明:

fun HorizontalPager(
    state: PagerState,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    pageSize: PageSize = PageSize.Fill,
    beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount,
    pageSpacing: Dp = 0.dp,
    verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
    flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state),
    userScrollEnabled: Boolean = true,
    reverseLayout: Boolean = false,
    key: ((index: Int) -> Any)? = null,
    pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(
        state,
        Orientation.Horizontal
    ),
    snapPosition: SnapPosition = SnapPosition.Start,
    pageContent: @Composable PagerScope.(page: Int) -> Unit
)

pagerState

表明当前Pager容器的状态,通过
rememberPagerState
来进行创建

此对象有以下几个常用的属性和方法:

  • currentPage
    :与贴靠位置最近的页面。默认情况下,贴靠位置位于布局的开头。

  • settledPage
    :未运行任何动画或滚动时显示的页码。这与 currentPage 属性不同,因为如果网页足够接近固定位置,currentPage 会立即更新,但 settledPage 会保持不变,直到所有动画都运行完毕。

  • targetPage
    :滚动动作的建议停止位置

  • scrollToPage(pageIndex)
    无动画指定滚动到指定页面下标(需要配合开启一个协程使用)

  • animateScrollToPage(pageIndex)
    带动画指定滚动到指定页面下标(需要配合开启一个协程使用)

val coroutineScope = rememberCoroutineScope()
coroutineScope.launch {
	// Call scroll to on pagerState
	pagerState.scrollToPage(5)
}

contentPadding(内边距)

感觉应该不用多说什么,和LazyRow等组件一样,用来设置内边距的

pageSize(页面item尺寸)

默认情况下,HorizontalPager 和 VerticalPager 分别占据整个宽度或整个高度。

可以将 pageSize 变量设置为使用 PageSize.Fixed、PageSize.Fill(默认)或自定义大小计算。

如,在HorizontalPager固定每个页面的宽度,使用
PageSize.Fixed(300.dp)

beyondViewportPageCount

接收一个整数,默认为0,即表示:
默认情况下,Pager只会在屏幕上加载可见的页面。

如需在屏幕外加载更多页面,请将 beyondBoundsPageCount 设置为大于零的值

userScrollEnabled

用来控制是否允许用户滚动的一个boolean数值

pageSpacing

接收一个dp数值, 每个页面item的间隔距离

代码示例补充

banner水平轮播

这个是比较常见的效果了,如下图所示:

代码如下:

val pagerState = rememberPagerState(pageCount = {
	4
})

Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
	HorizontalPager(state = pagerState, contentPadding = PaddingValues(horizontal = 32.dp), pageSpacing = 16.dp) { page -> // Our page content
		val bgColor = when (page) {
			0 -> {
				Color.Blue
			}

			1 -> {
				Color.Yellow
			}

			else -> {
				Color.LightGray
			}
		}
		
		Box(modifier = Modifier
			.fillMaxWidth()
			.height(100.dp)
			.background(bgColor))
	}

	Spacer(modifier = Modifier.height(16.dp))

	CustomIndicator(pagerState)
}

如果想自动轮播的效果,可以使用副作用函数配合,如下代码:

val pagerState = rememberPagerState(pageCount = {
        3
    })
	
LaunchedEffect(Unit) {
	while (true){
		//间隔1s跳转到下一个页面
		delay(1000)
		 if (pagerState.currentPage ==pagerState.pageCount-1) {
			 //重置的话,跳转不使用动画
			pagerState.scrollToPage(0)
		}else{
			pagerState.animateScrollToPage(pagerState.currentPage+1)
		}
	}
}
	
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
	HorizontalPager(state = pagerState, contentPadding = PaddingValues(horizontal = 32.dp), pageSpacing = 16.dp) { page -> // Our page content
		val bgColor = when (page) {
			0 -> {
				Color.Blue
			}

			1 -> {
				Color.Yellow
			}

			else -> {
				Color.LightGray
			}
		}
		Box(modifier = Modifier
			.fillMaxWidth()
			.height(100.dp)
			.background(bgColor))
	}

	Spacer(modifier = Modifier.height(16.dp))

	CustomIndicator(pagerState)
}
	

滚动小圆点指示器

/**
 * 指示器布局,与[HorizontalPager]
 *
 * @param pagerState pager的状态
 * @param modifier
 * @param activeColor 选中颜色
 * @param inactiveColor 未选中颜色
 * @param indicatorWidth 单个指示器的宽度
 * @param indicatorHeight 单个指示器的高度
 * @param spacing 每个指示器间隔
 * @param indicatorShape 指示器的形状
 *
 * @sample CustomIndicatorSample
 */

@Composable
fun CustomIndicator(
    pagerState: PagerState,
    modifier: Modifier = Modifier,
    activeColor: Color = MaterialTheme.colorScheme.primary,
    inactiveColor: Color = Color.LightGray,
    indicatorWidth: Dp = 10.dp,
    indicatorHeight: Dp = 5.dp,
    spacing: Dp = 5.dp,
    indicatorShape: Shape = CircleShape,
) {
    val spacingPx = LocalDensity.current.run { spacing.roundToPx() }

    Box(
        modifier = modifier, contentAlignment = Alignment.CenterStart
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(spacing),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            val indicatorModifier = Modifier.background(color = inactiveColor, shape = indicatorShape)

            //不能活动的索引的点
            repeat(pagerState.pageCount) {
                Box(
                    indicatorModifier.size(
                        indicatorWidth, indicatorHeight
                    )
                )
            }
        }

        //计算偏移量
        val scrollPosition = (pagerState.currentPage + pagerState.currentPageOffsetFraction).coerceIn(
                0f, (pagerState.pageCount - 1).coerceAtLeast(0).toFloat()
            ) //可以活动的索引点
        Box(Modifier
            .offset {
                IntOffset(
                    x = (spacingPx * scrollPosition + indicatorWidth.roundToPx() * scrollPosition).toInt(), y = 0
                )
            }
            .size(width = indicatorWidth, height = indicatorHeight)
            .background(
                color = activeColor,
                shape = indicatorShape,
            ))
    }
}

//使用示例
@Composable
private fun CustomIndicatorSample() {
    val pagerState = rememberPagerState(pageCount = {
        10
    })

    Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
        HorizontalPager(state = pagerState) { page ->
            Text("page=$page")
        }

        CustomIndicator(pagerState)
    }
}

两侧页面高度减小,且带动画效果

效果不太好描述,直接上动图

添加一个Modifier的扩展方法,然后给Pager里的Item使用即可

fun Modifier.carouselTransition(page: Int, pagerState: PagerState) =
    graphicsLayer {
        val pageOffset =
            ((pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue

        val transformation =
            lerp(
                start = 0.7f,
                stop = 1f,
                fraction = 1f - pageOffset.coerceIn(0f, 1f)
            )
        alpha = transformation
        scaleY = transformation
    }

示例:

val pageState = rememberPagerState(){3}
    Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
        HorizontalPager(state = pageState, contentPadding = PaddingValues(horizontal = 32.dp), pageSpacing = 16.dp, snapPosition = SnapPosition.Center) { page -> // Our page content
            
            val bgColor = when (page) {
                0 -> {
                    Color.Blue
                }

                1 -> {
                    Color.Yellow
                }

                else -> {
                    Color.LightGray
                }
            }
            Box(modifier = Modifier
                .fillMaxWidth()
                .height(100.dp)
                .carouselTransition(page,pageState)
                .background(bgColor))
        }
    }

参考

说在前面:参考刘焕勇老师在 Github 上开源的项目

GitHub地址:
基于知识图谱的医疗问答系统

一、搭建 Neo4j 图数据库

1、方式选择

  • windows 使用 Neo4j Desktop (2024-12-09开始 Neo4j desktop 无法打开表现为三个/四个僵尸进程,查看本地日志会发现[403]无法获取到https://dist.neo4j.org/neo4j-desktop/win/latest.yml这个路径的资源。解决方案:断网打开 Neo4j Desktop /
    Neo4j Desktop 1.5.8 Launches Zombie Processes Only - Neo4j Graph Platform / Desktop - Neo4j Online Community
  • 云环境 dockerfile + docker-compose (部署构建简单易懂无需专注 jdk 版本,优先考虑)
  • 最终理想化:kubernetes 部署 (符合主流技术导向,虽说部署较复杂且多坑但是企业级以及行业主导地位等因素使用 k8s 部署还是最佳实践)

首次部署优先采用 dockerfile + docker-compose

2、Dockerfile+docker-compose部署neo4j容器

2.1、更新 yum 镜像源

rm -rf /etc/yum.repos.d/*
wget -O /etc/yum.repos.d/centos7.repo http://mirrors.aliyun.com/repo/Centos-7.repo
wget -O /etc/yum.repos.d/epel-7.repo http://mirrors.aliyun.com/repo/epel-7.repo
wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

2.2、安装 docker-ce 社区版

yum install -y docker-ce

2.3、配置镜像加速

cat > /etc/docker/daemon.json << EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "registry-mirrors": [
    "https://dockerhub.icu",
    "https://hub.rat.dev",
    "https://docker.wanpeng.top",
    "https://doublezonline.cloud",
    "https://docker.mrxn.net",
    "https://docker.anyhub.us.kg",
    "https://dislabaiot.xyz",
    "https://docker.fxxk.dedyn.io"
  ]
}
EOF

systemctl daemon-reload && systemctl restart docker && systemctl enable docker

2.4、安装 Docker Compose

Releases · docker/compose

2.4.1、下载 Docker Compose 二进制包

curl -L "https://github.com/docker/compose/releases/download/v2.5.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  • -L
    : 是
    curl
    的一个选项,表示跟随重定向。如果下载链接是重定向的,这个选项会让
    curl
    自动跟踪到最后的目标地址。
  • "https://github.com/docker/compose/releases/download/v2.5.1/docker-compose-$(uname -s)-$(uname -m)"
    : 这是Docker Compose的下载URL,其中
    v2.5.1
    指定了要下载的Docker Compose版本号。
    $(uname -s)

    $(uname -m)
    是shell命令,分别返回当前系统的类型(如
    Linux
    )和机器的硬件架构(如
    x86_64
    ),这样可以确保下载与当前系统架构相匹配的Docker Compose二进制文件。
  • -o /usr/local/bin/docker-compose
    :
    -o

    --output
    指定了下载文件的保存位置及名称。这里,文件会被保存为
    /usr/local/bin/docker-compose
    ,这是Docker Compose常见的安装路径,将其放在此处可以使其在PATH环境变量中,从而可以直接在命令行中通过
    docker-compose
    命令调用。

2.4.2、设置可执行权限

chmod +x /usr/local/bin/docker-compose

2.4.3、查看版本

docker-compose -v

2.5、创建目录结构

mkdir -p neo4j-docker/{conf,data,import,logs} && touch neo4j-docker/conf/neo4j.conf

chown -R neo4j:neo4j ./{conf,data,import,logs}

chmod 755 ./{conf,data,logs,import}

tree -L 2 neo4j-docker
neo4j-docker
├── conf
│   └── neo4j.conf
├── data
├── import
└── logs

2.6、编写neo4j.conf配置文件

cat > /root/neo4j-docker/conf/neo4j.conf <<  EOF
server.directories.import=/var/lib/neo4j/import
server.memory.pagecache.size=512M

server.default_listen_address=0.0.0.0
dbms.security.allow_csv_import_from_file_urls=true
server.directories.logs=/logs
EOF

2.7、编写 dockerfile 文件

cat > /root/neo4j-docker/Dockerfile << EOF
# 使用官方 Neo4j 最新版本镜像作为基础镜像
FROM neo4j:latest

# 设置环境变量,仅用于配置 Neo4j 认证
ENV NEO4J_AUTH=neo4j/neo4jpassword

# 拷贝本地的配置文件到容器中
COPY ./conf/neo4j.conf /var/lib/neo4j/conf/

# 定义容器启动时执行的命令
CMD ["neo4j"]
EOF

2.8、构建ne4j容器镜像

# 命令位置需要与Dockerfile位置同级
docker build -t my_neo4j:v1 .

image-20241210102548272

2.9、编写docker-compose.yaml文件

有坑:neo4j 5.x 版本所需密码位数需要在 8 位以上

version: '3'
services:
  neo4j:
    build: .
    image: my_neo4j:v1
    container_name: neo4j_container
    restart: always
    ports:
      - "7474:7474"
      - "7687:7687"
    environment:
      - NEO4J_AUTH=neo4j/neo4jpassword
    volumes:
      - ./data:/data
      - ./logs:/logs
      - ./import:/var/lib/neo4j/import
      - ./conf:/var/lib/neo4j/conf
    command: ["neo4j"]

2.10、运行docker-compose

docker-compose -f docker-compose.yaml up -d

2.11、浏览器登录 neo4j

http://192.168.112.30:7474

# 输入用户名:neo4j
# 输入密码:neo4jpassword

二、Neo4j 初始配置

1、清空 Neo4j 数据库

MATCH (n) DETACH DELETE n

image-20241217225238581

三、PyCharm 项目安装必备库

1、py2neo 库

pip install py2neo
  • 简化 Neo4j 连接和查询


    • 连接到 Neo4j

      py2neo
      提供了简单易用的接口来连接到 Neo4j 数据库,支持 HTTP 和 Bolt 协议。
    • 执行 Cypher 查询

      py2neo
      允许你直接执行
      Cypher
      查询(Neo4j 的图查询语言),并以 Python 对象的形式返回结果。
  • 创建和管理图数据


    • 创建节点和关系

      py2neo
      提供了高级抽象,允许你像操作 Python 对象一样创建和管理 Neo4j 中的节点和关系。你可以使用
      Node

      Relationship
      类来表示图中的实体,并将它们保存到数据库中。
    • 批量操作

      py2neo
      支持批量创建节点和关系,提高性能,减少网络往返次数。

2、pymongo 库

pip install pymongo
  • 用于连接和操作 MongoDB 数据库,读取、处理并重新插入医疗数据。
  • 提供了高效的 CRUD 操作,支持批量数据处理。

3、lxml 库

pip install lxml
  • 用于解析存储在 MongoDB 中的 HTML 文档,提取有用的医疗检查信息(如疾病名称、描述等)。
  • 通过 XPath 提取数据,并进行必要的清理和格式化。

四、python 连接 Neo4j

1、浏览器 browser 查看Neo4j 连接状态

:server status

image-20241217231334624

记住 URL (不是传统意义上的 http://,以及默认的端口号7474)

2、修改源文件中 Graph 连接格式

import os
import json
from py2neo import Graph,Node

class MedicalGraph:
    def __init__(self):
        cur_dir = '/'.join(os.path.abspath(__file__).split('/')[:-1])
        self.data_path = os.path.join(cur_dir, 'data/medical.json')
        self.g = Graph("neo4j://192.168.112.30:7687", auth=("neo4j", "neo4jpassword"))

build_medicalgraph.py

answer_search.py
两个原文件中的
self.g = Graph()
的连接格式都更改为上述代码中的格式。

五、PyCharm 导入医疗知识图谱

1、读取文件

# 读取文件
    def read_nodes(self):
        # 共7类节点
        drugs = [] # 药品
        foods = [] # 食物
        checks = [] # 检查
        departments = [] #科室
        producers = [] #药品大类
        diseases = [] #疾病
        symptoms = []#症状

        disease_infos = []#疾病信息

        # 构建节点实体关系
        rels_department = [] # 科室-科室关系
        rels_noteat = [] # 疾病-忌吃食物关系
        rels_doeat = [] # 疾病-宜吃食物关系
        rels_recommandeat = [] # 疾病-推荐吃食物关系
        rels_commonddrug = [] # 疾病-通用药品关系
        rels_recommanddrug = [] # 疾病-热门药品关系
        rels_check = [] # 疾病-检查关系
        rels_drug_producer = [] # 厂商-药物关系

        rels_symptom = [] #疾病症状关系
        rels_acompany = [] # 疾病并发关系
        rels_category = [] # 疾病与科室之间的关系


        count = 0
        for data in open(self.data_path, encoding='utf8', mode='r'):
            disease_dict = {}
            count += 1
            print(count)
            data_json = json.loads(data)
            disease = data_json['name']
            disease_dict['name'] = disease
            diseases.append(disease)
            disease_dict['desc'] = ''
            disease_dict['prevent'] = ''
            disease_dict['cause'] = ''
            disease_dict['easy_get'] = ''
            disease_dict['cure_department'] = ''
            disease_dict['cure_way'] = ''
            disease_dict['cure_lasttime'] = ''
            disease_dict['symptom'] = ''
            disease_dict['cured_prob'] = ''

            if 'symptom' in data_json:
                symptoms += data_json['symptom']
                for symptom in data_json['symptom']:
                    rels_symptom.append([disease, symptom])

            if 'acompany' in data_json:
                for acompany in data_json['acompany']:
                    rels_acompany.append([disease, acompany])

            if 'desc' in data_json:
                disease_dict['desc'] = data_json['desc']

            if 'prevent' in data_json:
                disease_dict['prevent'] = data_json['prevent']

            if 'cause' in data_json:
                disease_dict['cause'] = data_json['cause']

            if 'get_prob' in data_json:
                disease_dict['get_prob'] = data_json['get_prob']

            if 'easy_get' in data_json:
                disease_dict['easy_get'] = data_json['easy_get']

            if 'cure_department' in data_json:
                cure_department = data_json['cure_department']
                if len(cure_department) == 1:
                     rels_category.append([disease, cure_department[0]])
                if len(cure_department) == 2:
                    big = cure_department[0]
                    small = cure_department[1]
                    rels_department.append([small, big])
                    rels_category.append([disease, small])

                disease_dict['cure_department'] = cure_department
                departments += cure_department

            if 'cure_way' in data_json:
                disease_dict['cure_way'] = data_json['cure_way']

            if  'cure_lasttime' in data_json:
                disease_dict['cure_lasttime'] = data_json['cure_lasttime']

            if 'cured_prob' in data_json:
                disease_dict['cured_prob'] = data_json['cured_prob']

            if 'common_drug' in data_json:
                common_drug = data_json['common_drug']
                for drug in common_drug:
                    rels_commonddrug.append([disease, drug])
                drugs += common_drug

            if 'recommand_drug' in data_json:
                recommand_drug = data_json['recommand_drug']
                drugs += recommand_drug
                for drug in recommand_drug:
                    rels_recommanddrug.append([disease, drug])

            if 'not_eat' in data_json:
                not_eat = data_json['not_eat']
                for _not in not_eat:
                    rels_noteat.append([disease, _not])

                foods += not_eat
                do_eat = data_json['do_eat']
                for _do in do_eat:
                    rels_doeat.append([disease, _do])

                foods += do_eat
                recommand_eat = data_json['recommand_eat']

                for _recommand in recommand_eat:
                    rels_recommandeat.append([disease, _recommand])
                foods += recommand_eat

            if 'check' in data_json:
                check = data_json['check']
                for _check in check:
                    rels_check.append([disease, _check])
                checks += check
            if 'drug_detail' in data_json:
                drug_detail = data_json['drug_detail']
                producer = [i.split('(')[0] for i in drug_detail]
                rels_drug_producer += [[i.split('(')[0], i.split('(')[-1].replace(')', '')] for i in drug_detail]
                producers += producer
            disease_infos.append(disease_dict)
        return set(drugs), set(foods), set(checks), set(departments), set(producers), set(symptoms), set(diseases), disease_infos,\
               rels_check, rels_recommandeat, rels_noteat, rels_doeat, rels_department, rels_commonddrug, rels_drug_producer, rels_recommanddrug,\
               rels_symptom, rels_acompany, rels_category

2、建立节点

# 建立节点
    def create_node(self, label, nodes):
        count = 0
        for node_name in nodes:
            node = Node(label, name=node_name)
            self.g.create(node)
            count += 1
            print(count, len(nodes))
        return

3、创建知识图谱中心疾病的节点

# 创建知识图谱中心疾病的节点
    def create_diseases_nodes(self, disease_infos):
        count = 0
        for disease_dict in disease_infos:
            node = Node("Disease", name=disease_dict['name'], desc=disease_dict['desc'],
                        prevent=disease_dict['prevent'] ,cause=disease_dict['cause'],
                        easy_get=disease_dict['easy_get'],cure_lasttime=disease_dict['cure_lasttime'],
                        cure_department=disease_dict['cure_department']
                        ,cure_way=disease_dict['cure_way'] , cured_prob=disease_dict['cured_prob'])
            self.g.create(node)
            count += 1
            print(count)
        return

4、创建知识图谱实体节点类型schema

# 创建知识图谱实体节点类型schema
    def create_graphnodes(self):
        Drugs, Foods, Checks, Departments, Producers, Symptoms, Diseases, disease_infos,rels_check, rels_recommandeat, rels_noteat, rels_doeat, rels_department, rels_commonddrug, rels_drug_producer, rels_recommanddrug,rels_symptom, rels_acompany, rels_category = self.read_nodes()
        self.create_diseases_nodes(disease_infos)
        self.create_node('Drug', Drugs)
        print(len(Drugs))
        self.create_node('Food', Foods)
        print(len(Foods))
        self.create_node('Check', Checks)
        print(len(Checks))
        self.create_node('Department', Departments)
        print(len(Departments))
        self.create_node('Producer', Producers)
        print(len(Producers))
        self.create_node('Symptom', Symptoms)
        return

5、创建实体关系边

# 创建实体关系边
    def create_graphrels(self):
        Drugs, Foods, Checks, Departments, Producers, Symptoms, Diseases, disease_infos, rels_check, rels_recommandeat, rels_noteat, rels_doeat, rels_department, rels_commonddrug, rels_drug_producer, rels_recommanddrug,rels_symptom, rels_acompany, rels_category = self.read_nodes()
        self.create_relationship('Disease', 'Food', rels_recommandeat, 'recommand_eat', '推荐食谱')
        self.create_relationship('Disease', 'Food', rels_noteat, 'no_eat', '忌吃')
        self.create_relationship('Disease', 'Food', rels_doeat, 'do_eat', '宜吃')
        self.create_relationship('Department', 'Department', rels_department, 'belongs_to', '属于')
        self.create_relationship('Disease', 'Drug', rels_commonddrug, 'common_drug', '常用药品')
        self.create_relationship('Producer', 'Drug', rels_drug_producer, 'drugs_of', '生产药品')
        self.create_relationship('Disease', 'Drug', rels_recommanddrug, 'recommand_drug', '好评药品')
        self.create_relationship('Disease', 'Check', rels_check, 'need_check', '诊断检查')
        self.create_relationship('Disease', 'Symptom', rels_symptom, 'has_symptom', '症状')
        self.create_relationship('Disease', 'Disease', rels_acompany, 'acompany_with', '并发症')
        self.create_relationship('Disease', 'Department', rels_category, 'belongs_to', '所属科室')

6、创建实体关联边

# 创建实体关联边
    def create_relationship(self, start_node, end_node, edges, rel_type, rel_name):
        count = 0
        # 去重处理
        set_edges = []
        for edge in edges:
            set_edges.append('###'.join(edge))
        all = len(set(set_edges))
        for edge in set(set_edges):
            edge = edge.split('###')
            p = edge[0]
            q = edge[1]
            query = "match(p:%s),(q:%s) where p.name='%s'and q.name='%s' create (p)-[rel:%s{name:'%s'}]->(q)" % (
                start_node, end_node, p, q, rel_type, rel_name)
            try:
                self.g.run(query)
                count += 1
                print(rel_type, count, all)
            except Exception as e:
                print(e)
        return

7、导出数据

# 导出数据
    def export_data(self):
        Drugs, Foods, Checks, Departments, Producers, Symptoms, Diseases, disease_infos, rels_check, rels_recommandeat, rels_noteat, rels_doeat, rels_department, rels_commonddrug, rels_drug_producer, rels_recommanddrug, rels_symptom, rels_acompany, rels_category = self.read_nodes()
        f_drug = open('drug.txt', 'w+')
        f_food = open('food.txt', 'w+')
        f_check = open('check.txt', 'w+')
        f_department = open('department.txt', 'w+')
        f_producer = open('producer.txt', 'w+')
        f_symptom = open('symptoms.txt', 'w+')
        f_disease = open('disease.txt', 'w+')

        f_drug.write('\n'.join(list(Drugs)))
        f_food.write('\n'.join(list(Foods)))
        f_check.write('\n'.join(list(Checks)))
        f_department.write('\n'.join(list(Departments)))
        f_producer.write('\n'.join(list(Producers)))
        f_symptom.write('\n'.join(list(Symptoms)))
        f_disease.write('\n'.join(list(Diseases)))

        f_drug.close()
        f_food.close()
        f_check.close()
        f_department.close()
        f_producer.close()
        f_symptom.close()
        f_disease.close()

        return

8、程序主入口

if __name__ == '__main__':
    handler = MedicalGraph()
    print("step1:导入图谱节点中")
    handler.create_graphnodes()
    print("step2:导入图谱边中")      
    handler.create_graphrels()
# 创建知识节点和边(nodes + rels)
# handler.create_graphnodes()
# handler.create_graphrels()
快捷键:Ctrl + Shift + F10

8.1、UnicodeDecodeError: 'gbk' codec can't decode byte 0xaf in position 81: illegal multibyte sequence

直接运行会报错:UnicodeDecodeError: 'gbk' codec can't decode byte 0xaf in position 81: illegal multibyte sequence

8.2、修改代码:for data in open(self.data_path):

for data in open(self.data_path, encoding='utf8', mode='r'):
  • 需要确保文件的编码格式为 utf8
  • 打开文件模式为只读模式

9、运行结果

image-20241217234528761

10、优化导入数据时间

import concurrent
import concurrent.futures
import json
import multiprocessing
import os

from py2neo import Graph, Node, Subgraph
from tqdm import tqdm


class MedicalGraph:
    def __init__(self):
        pass

    def clear(self):
        self.g.run("MATCH (n) DETACH DELETE n")

    '''读取文件'''

    def read_nodes(self):
        # 共7类节点
        drugs = []  # 药品
        foods = []  # 食物
        checks = []  # 检查
        departments = []  # 科室
        producers = []  # 药品大类
        diseases = []  # 疾病
        symptoms = []  # 症状

        disease_infos = []  # 疾病信息

        # 构建节点实体关系
        rels_department = []  # 科室-科室关系
        rels_noteat = []  # 疾病-忌吃食物关系
        rels_doeat = []  # 疾病-宜吃食物关系
        rels_recommandeat = []  # 疾病-推荐吃食物关系
        rels_commonddrug = []  # 疾病-通用药品关系
        rels_recommanddrug = []  # 疾病-热门药品关系
        rels_check = []  # 疾病-检查关系
        rels_drug_producer = []  # 厂商-药物关系

        rels_symptom = []  # 疾病症状关系
        rels_acompany = []  # 疾病并发关系
        rels_category = []  # 疾病与科室之间的关系

        for data in open(self.data_path):
            disease_dict = {}
            data_json = json.loads(data)
            disease = data_json['name']
            disease_dict['name'] = disease
            diseases.append(disease)
            disease_dict['desc'] = ''
            disease_dict['prevent'] = ''
            disease_dict['cause'] = ''
            disease_dict['easy_get'] = ''
            disease_dict['cure_department'] = ''
            disease_dict['cure_way'] = ''
            disease_dict['cure_lasttime'] = ''
            disease_dict['symptom'] = ''
            disease_dict['cured_prob'] = ''

            if 'symptom' in data_json:
                symptoms += data_json['symptom']
                for symptom in data_json['symptom']:
                    rels_symptom.append([disease, symptom])

            if 'acompany' in data_json:
                for acompany in data_json['acompany']:
                    rels_acompany.append([disease, acompany])

            if 'desc' in data_json:
                disease_dict['desc'] = data_json['desc']

            if 'prevent' in data_json:
                disease_dict['prevent'] = data_json['prevent']

            if 'cause' in data_json:
                disease_dict['cause'] = data_json['cause']

            if 'get_prob' in data_json:
                disease_dict['get_prob'] = data_json['get_prob']

            if 'easy_get' in data_json:
                disease_dict['easy_get'] = data_json['easy_get']

            if 'cure_department' in data_json:
                cure_department = data_json['cure_department']
                if len(cure_department) == 1:
                    rels_category.append([disease, cure_department[0]])
                if len(cure_department) == 2:
                    big = cure_department[0]
                    small = cure_department[1]
                    rels_department.append([small, big])
                    rels_category.append([disease, small])

                disease_dict['cure_department'] = cure_department
                departments += cure_department

            if 'cure_way' in data_json:
                disease_dict['cure_way'] = data_json['cure_way']

            if 'cure_lasttime' in data_json:
                disease_dict['cure_lasttime'] = data_json['cure_lasttime']

            if 'cured_prob' in data_json:
                disease_dict['cured_prob'] = data_json['cured_prob']

            if 'common_drug' in data_json:
                common_drug = data_json['common_drug']
                for drug in common_drug:
                    rels_commonddrug.append([disease, drug])
                drugs += common_drug

            if 'recommand_drug' in data_json:
                recommand_drug = data_json['recommand_drug']
                drugs += recommand_drug
                for drug in recommand_drug:
                    rels_recommanddrug.append([disease, drug])

            if 'not_eat' in data_json:
                not_eat = data_json['not_eat']
                for _not in not_eat:
                    rels_noteat.append([disease, _not])

                foods += not_eat
                do_eat = data_json['do_eat']
                for _do in do_eat:
                    rels_doeat.append([disease, _do])

                foods += do_eat
                recommand_eat = data_json['recommand_eat']

                for _recommand in recommand_eat:
                    rels_recommandeat.append([disease, _recommand])
                foods += recommand_eat

            if 'check' in data_json:
                check = data_json['check']
                for _check in check:
                    rels_check.append([disease, _check])
                checks += check
            if 'drug_detail' in data_json:
                drug_detail = data_json['drug_detail']
                producer = [i.split('(')[0] for i in drug_detail]
                rels_drug_producer += [[i.split('(')[0], i.split('(')[-1].replace(')', '')] for i in drug_detail]
                producers += producer
            disease_infos.append(disease_dict)
        return set(drugs), set(foods), set(checks), set(departments), set(producers), set(symptoms), set(diseases), disease_infos, \
            rels_check, rels_recommandeat, rels_noteat, rels_doeat, rels_department, rels_commonddrug, rels_drug_producer, rels_recommanddrug, \
            rels_symptom, rels_acompany, rels_category

    '''建立节点'''

    def create_node(self, label, nodes):
        batch_size = 1000
        batches = [list(nodes)[i:i + batch_size] for i in range(0, len(nodes), batch_size)]
        for batch in tqdm(batches, desc=f"Creating {label} Nodes", unit="batch"):
            batch_nodes = [Node(label, name=node_name) for node_name in batch]
            self.g.create(Subgraph(batch_nodes))

    '''创建知识图谱中心疾病的节点'''

    def create_diseases_nodes(self, disease_infos):
        batch_size = 1000
        batches = [disease_infos[i:i + batch_size] for i in range(0, len(disease_infos), batch_size)]
        for batch in tqdm(batches, desc="Importing Disease Nodes", unit="batch"):
            batch_nodes = [
                Node("Disease", name=disease_dict['name'], desc=disease_dict['desc'],
                     prevent=disease_dict['prevent'], cause=disease_dict['cause'],
                     easy_get=disease_dict['easy_get'], cure_lasttime=disease_dict['cure_lasttime'],
                     cure_department=disease_dict['cure_department'], cure_way=disease_dict['cure_way'],
                     cured_prob=disease_dict['cured_prob']) for disease_dict in batch
            ]
            self.g.create(Subgraph(batch_nodes))

    '''创建知识图谱实体节点类型schema'''

    def create_graphnodes(self):
        Drugs, Foods, Checks, Departments, Producers, Symptoms, Diseases, disease_infos, rels_check, rels_recommandeat, rels_noteat, rels_doeat, rels_department, rels_commonddrug, rels_drug_producer, rels_recommanddrug, rels_symptom, rels_acompany, rels_category = self.read_nodes()
        self.create_diseases_nodes(disease_infos)
        self.create_node('Drug', Drugs)
        self.create_node('Food', Foods)
        self.create_node('Check', Checks)
        self.create_node('Department', Departments)
        self.create_node('Producer', Producers)
        self.create_node('Symptom', Symptoms)

    '''创建实体关系边'''

    def create_graphrels(self):
        Drugs, Foods, Checks, Departments, Producers, Symptoms, Diseases, disease_infos, rels_check, rels_recommandeat, rels_noteat, rels_doeat, rels_department, rels_commonddrug, rels_drug_producer, rels_recommanddrug, rels_symptom, rels_acompany, rels_category = self.read_nodes()
        self.create_relationship('Disease', 'Food', rels_recommandeat, 'recommand_eat', '推荐食谱')
        self.create_relationship('Disease', 'Food', rels_noteat, 'no_eat', '忌吃')
        self.create_relationship('Disease', 'Food', rels_doeat, 'do_eat', '宜吃')
        self.create_relationship('Department', 'Department', rels_department, 'belongs_to', '属于')
        self.create_relationship('Disease', 'Drug', rels_commonddrug, 'common_drug', '常用药品')
        self.create_relationship('Producer', 'Drug', rels_drug_producer, 'drugs_of', '生产药品')
        self.create_relationship('Disease', 'Drug', rels_recommanddrug, 'recommand_drug', '好评药品')
        self.create_relationship('Disease', 'Check', rels_check, 'need_check', '诊断检查')
        self.create_relationship('Disease', 'Symptom', rels_symptom, 'has_symptom', '症状')
        self.create_relationship('Disease', 'Disease', rels_acompany, 'acompany_with', '并发症')
        self.create_relationship('Disease', 'Department', rels_category, 'belongs_to', '所属科室')

    '''创建实体关联边'''

    def create_relationship(self, start_node, end_node, edges, rel_type, rel_name):
        batch_size = 10000
        set_edges = set(['###'.join(edge) for edge in edges])
        batches = [list(set_edges)[i:i + batch_size] for i in range(0, len(set_edges), batch_size)]
        executor = concurrent.futures.ThreadPoolExecutor(max_workers=min(multiprocessing.cpu_count(), 4))
        tasks = [
            lambda: (
                tx := self.g.begin(),
                [
                    tx.run(
                        f"MATCH (p:{start_node}), (q:{end_node}) "
                        f"WHERE p.name='{p}' AND q.name='{q}' "
                        f"CREATE (p)-[rel:{rel_type} {{name:'{rel_name}'}}]->(q)"
                    ) for edge in batch for p, q in [edge.split('###')]
                ],
                self.g.commit(tx)
            ) for batch in tqdm(batches, desc=f"Creating {rel_type} Relationships", unit="batch")
        ]
        executor.map(lambda task: task(), tasks)
        executor.shutdown()

    '''导出数据'''

    def export_data(self):
        Drugs, Foods, Checks, Departments, Producers, Symptoms, Diseases, disease_infos, rels_check, rels_recommandeat, rels_noteat, rels_doeat, rels_department, rels_commonddrug, rels_drug_producer, rels_recommanddrug, rels_symptom, rels_acompany, rels_category = self.read_nodes()
        f_drug = open('drug.txt', 'w+')
        f_food = open('food.txt', 'w+')
        f_check = open('check.txt', 'w+')
        f_department = open('department.txt', 'w+')
        f_producer = open('producer.txt', 'w+')
        f_symptom = open('symptoms.txt', 'w+')
        f_disease = open('disease.txt', 'w+')

        f_drug.write('\n'.join(list(Drugs)))
        f_food.write('\n'.join(list(Foods)))
        f_check.write('\n'.join(list(Checks)))
        f_department.write('\n'.join(list(Departments)))
        f_producer.write('\n'.join(list(Producers)))
        f_symptom.write('\n'.join(list(Symptoms)))
        f_disease.write('\n'.join(list(Diseases)))

        f_drug.close()
        f_food.close()
        f_check.close()
        f_department.close()
        f_producer.close()
        f_symptom.close()
        f_disease.close()


if __name__ == '__main__':
    handler = MedicalGraph()
    handler.clear()
    print("step1:导入图谱节点中")
    handler.create_graphnodes()
    print("step2:导入图谱边中")
    handler.create_graphrels()

六、PyCharm 实现问答系统

1、问句类型分类脚本

这里
加载多个特征词列表
处需要保证文件编码格式为
utf8

即添加内容:encoding='utf8'

import os
import ahocorasick

class QuestionClassifier:
    def __init__(self):
        cur_dir = '/'.join(os.path.abspath(__file__).split('/')[:-1])
        # 特征词路径
        self.disease_path = os.path.join(cur_dir, 'dict/disease.txt')
        self.department_path = os.path.join(cur_dir, 'dict/department.txt')
        self.check_path = os.path.join(cur_dir, 'dict/check.txt')
        self.drug_path = os.path.join(cur_dir, 'dict/drug.txt')
        self.food_path = os.path.join(cur_dir, 'dict/food.txt')
        self.producer_path = os.path.join(cur_dir, 'dict/producer.txt')
        self.symptom_path = os.path.join(cur_dir, 'dict/symptom.txt')
        self.deny_path = os.path.join(cur_dir, 'dict/deny.txt')
        # 加载特征词
        self.disease_wds= [i.strip() for i in open(self.disease_path,encoding='utf8') if i.strip()]
        self.department_wds= [i.strip() for i in open(self.department_path,encoding='utf8') if i.strip()]
        self.check_wds= [i.strip() for i in open(self.check_path,encoding='utf8') if i.strip()]
        self.drug_wds= [i.strip() for i in open(self.drug_path,encoding='utf8') if i.strip()]
        self.food_wds= [i.strip() for i in open(self.food_path,encoding='utf8') if i.strip()]
        self.producer_wds= [i.strip() for i in open(self.producer_path,encoding='utf8') if i.strip()]
        self.symptom_wds= [i.strip() for i in open(self.symptom_path,encoding='utf8') if i.strip()]
        self.region_words = set(self.department_wds + self.disease_wds + self.check_wds + self.drug_wds + self.food_wds + self.producer_wds + self.symptom_wds)
        self.deny_words = [i.strip() for i in open(self.deny_path,encoding='utf8') if i.strip()]
        # 构造领域actree
        self.region_tree = self.build_actree(list(self.region_words))
        # 构建词典
        self.wdtype_dict = self.build_wdtype_dict()
        # 问句疑问词
        self.symptom_qwds = ['症状', '表征', '现象', '症候', '表现']
        self.cause_qwds = ['原因','成因', '为什么', '怎么会', '怎样才', '咋样才', '怎样会', '如何会', '为啥', '为何', '如何才会', '怎么才会', '会导致', '会造成']
        self.acompany_qwds = ['并发症', '并发', '一起发生', '一并发生', '一起出现', '一并出现', '一同发生', '一同出现', '伴随发生', '伴随', '共现']
        self.food_qwds = ['饮食', '饮用', '吃', '食', '伙食', '膳食', '喝', '菜' ,'忌口', '补品', '保健品', '食谱', '菜谱', '食用', '食物','补品']
        self.drug_qwds = ['药', '药品', '用药', '胶囊', '口服液', '炎片']
        self.prevent_qwds = ['预防', '防范', '抵制', '抵御', '防止','躲避','逃避','避开','免得','逃开','避开','避掉','躲开','躲掉','绕开',
                             '怎样才能不', '怎么才能不', '咋样才能不','咋才能不', '如何才能不',
                             '怎样才不', '怎么才不', '咋样才不','咋才不', '如何才不',
                             '怎样才可以不', '怎么才可以不', '咋样才可以不', '咋才可以不', '如何可以不',
                             '怎样才可不', '怎么才可不', '咋样才可不', '咋才可不', '如何可不']
        self.lasttime_qwds = ['周期', '多久', '多长时间', '多少时间', '几天', '几年', '多少天', '多少小时', '几个小时', '多少年']
        self.cureway_qwds = ['怎么治疗', '如何医治', '怎么医治', '怎么治', '怎么医', '如何治', '医治方式', '疗法', '咋治', '怎么办', '咋办', '咋治']
        self.cureprob_qwds = ['多大概率能治好', '多大几率能治好', '治好希望大么', '几率', '几成', '比例', '可能性', '能治', '可治', '可以治', '可以医']
        self.easyget_qwds = ['易感人群', '容易感染', '易发人群', '什么人', '哪些人', '感染', '染上', '得上']
        self.check_qwds = ['检查', '检查项目', '查出', '检查', '测出', '试出']
        self.belong_qwds = ['属于什么科', '属于', '什么科', '科室']
        self.cure_qwds = ['治疗什么', '治啥', '治疗啥', '医治啥', '治愈啥', '主治啥', '主治什么', '有什么用', '有何用', '用处', '用途',
                          '有什么好处', '有什么益处', '有何益处', '用来', '用来做啥', '用来作甚', '需要', '要']

        print('model init finished ......')

        return

    '''分类主函数'''
    def classify(self, question):
        data = {}
        medical_dict = self.check_medical(question)
        if not medical_dict:
            return {}
        data['args'] = medical_dict
        #收集问句当中所涉及到的实体类型
        types = []
        for type_ in medical_dict.values():
            types += type_
        question_type = 'others'

        question_types = []

        # 症状
        if self.check_words(self.symptom_qwds, question) and ('disease' in types):
            question_type = 'disease_symptom'
            question_types.append(question_type)

        if self.check_words(self.symptom_qwds, question) and ('symptom' in types):
            question_type = 'symptom_disease'
            question_types.append(question_type)

        # 原因
        if self.check_words(self.cause_qwds, question) and ('disease' in types):
            question_type = 'disease_cause'
            question_types.append(question_type)
        # 并发症
        if self.check_words(self.acompany_qwds, question) and ('disease' in types):
            question_type = 'disease_acompany'
            question_types.append(question_type)

        # 推荐食品
        if self.check_words(self.food_qwds, question) and 'disease' in types:
            deny_status = self.check_words(self.deny_words, question)
            if deny_status:
                question_type = 'disease_not_food'
            else:
                question_type = 'disease_do_food'
            question_types.append(question_type)

        #已知食物找疾病
        if self.check_words(self.food_qwds+self.cure_qwds, question) and 'food' in types:
            deny_status = self.check_words(self.deny_words, question)
            if deny_status:
                question_type = 'food_not_disease'
            else:
                question_type = 'food_do_disease'
            question_types.append(question_type)

        # 推荐药品
        if self.check_words(self.drug_qwds, question) and 'disease' in types:
            question_type = 'disease_drug'
            question_types.append(question_type)

        # 药品治啥病
        if self.check_words(self.cure_qwds, question) and 'drug' in types:
            question_type = 'drug_disease'
            question_types.append(question_type)

        # 疾病接受检查项目
        if self.check_words(self.check_qwds, question) and 'disease' in types:
            question_type = 'disease_check'
            question_types.append(question_type)

        # 已知检查项目查相应疾病
        if self.check_words(self.check_qwds+self.cure_qwds, question) and 'check' in types:
            question_type = 'check_disease'
            question_types.append(question_type)

        # 症状防御
        if self.check_words(self.prevent_qwds, question) and 'disease' in types:
            question_type = 'disease_prevent'
            question_types.append(question_type)

        # 疾病医疗周期
        if self.check_words(self.lasttime_qwds, question) and 'disease' in types:
            question_type = 'disease_lasttime'
            question_types.append(question_type)

        # 疾病治疗方式
        if self.check_words(self.cureway_qwds, question) and 'disease' in types:
            question_type = 'disease_cureway'
            question_types.append(question_type)

        # 疾病治愈可能性
        if self.check_words(self.cureprob_qwds, question) and 'disease' in types:
            question_type = 'disease_cureprob'
            question_types.append(question_type)

        # 疾病易感染人群
        if self.check_words(self.easyget_qwds, question) and 'disease' in types :
            question_type = 'disease_easyget'
            question_types.append(question_type)

        # 若没有查到相关的外部查询信息,那么则将该疾病的描述信息返回
        if question_types == [] and 'disease' in types:
            question_types = ['disease_desc']

        # 若没有查到相关的外部查询信息,那么则将该疾病的描述信息返回
        if question_types == [] and 'symptom' in types:
            question_types = ['symptom_disease']

        # 将多个分类结果进行合并处理,组装成一个字典
        data['question_types'] = question_types

        return data

    '''构造词对应的类型'''
    def build_wdtype_dict(self):
        wd_dict = dict()
        for wd in self.region_words:
            wd_dict[wd] = []
            if wd in self.disease_wds:
                wd_dict[wd].append('disease')
            if wd in self.department_wds:
                wd_dict[wd].append('department')
            if wd in self.check_wds:
                wd_dict[wd].append('check')
            if wd in self.drug_wds:
                wd_dict[wd].append('drug')
            if wd in self.food_wds:
                wd_dict[wd].append('food')
            if wd in self.symptom_wds:
                wd_dict[wd].append('symptom')
            if wd in self.producer_wds:
                wd_dict[wd].append('producer')
        return wd_dict

    '''构造actree,加速过滤'''
    def build_actree(self, wordlist):
        actree = ahocorasick.Automaton()
        for index, word in enumerate(wordlist):
            actree.add_word(word, (index, word))
        actree.make_automaton()
        return actree

    '''问句过滤'''
    def check_medical(self, question):
        region_wds = []
        for i in self.region_tree.iter(question):
            wd = i[1][1]
            region_wds.append(wd)
        stop_wds = []
        for wd1 in region_wds:
            for wd2 in region_wds:
                if wd1 in wd2 and wd1 != wd2:
                    stop_wds.append(wd1)
        final_wds = [i for i in region_wds if i not in stop_wds]
        final_dict = {i:self.wdtype_dict.get(i) for i in final_wds}

        return final_dict

    '''基于特征词进行分类'''
    def check_words(self, wds, sent):
        for wd in wds:
            if wd in sent:
                return True
        return False


if __name__ == '__main__':
    handler = QuestionClassifier()
    while 1:
        question = input('input an question:')
        data = handler.classify(question)
        print(data)

2、问句解析脚本

class QuestionPaser:

    '''构建实体节点'''
    def build_entitydict(self, args):
        entity_dict = {}
        for arg, types in args.items():
            for type in types:
                if type not in entity_dict:
                    entity_dict[type] = [arg]
                else:
                    entity_dict[type].append(arg)

        return entity_dict

    '''解析主函数'''
    def parser_main(self, res_classify):
        args = res_classify['args']
        entity_dict = self.build_entitydict(args)
        question_types = res_classify['question_types']
        sqls = []
        for question_type in question_types:
            sql_ = {}
            sql_['question_type'] = question_type
            sql = []
            if question_type == 'disease_symptom':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'symptom_disease':
                sql = self.sql_transfer(question_type, entity_dict.get('symptom'))

            elif question_type == 'disease_cause':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'disease_acompany':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'disease_not_food':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'disease_do_food':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'food_not_disease':
                sql = self.sql_transfer(question_type, entity_dict.get('food'))

            elif question_type == 'food_do_disease':
                sql = self.sql_transfer(question_type, entity_dict.get('food'))

            elif question_type == 'disease_drug':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'drug_disease':
                sql = self.sql_transfer(question_type, entity_dict.get('drug'))

            elif question_type == 'disease_check':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'check_disease':
                sql = self.sql_transfer(question_type, entity_dict.get('check'))

            elif question_type == 'disease_prevent':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'disease_lasttime':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'disease_cureway':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'disease_cureprob':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'disease_easyget':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            elif question_type == 'disease_desc':
                sql = self.sql_transfer(question_type, entity_dict.get('disease'))

            if sql:
                sql_['sql'] = sql

                sqls.append(sql_)

        return sqls

    '''针对不同的问题,分开进行处理'''
    def sql_transfer(self, question_type, entities):
        if not entities:
            return []

        # 查询语句
        sql = []
        # 查询疾病的原因
        if question_type == 'disease_cause':
            sql = ["MATCH (m:Disease) where m.name = '{0}' return m.name, m.cause".format(i) for i in entities]

        # 查询疾病的防御措施
        elif question_type == 'disease_prevent':
            sql = ["MATCH (m:Disease) where m.name = '{0}' return m.name, m.prevent".format(i) for i in entities]

        # 查询疾病的持续时间
        elif question_type == 'disease_lasttime':
            sql = ["MATCH (m:Disease) where m.name = '{0}' return m.name, m.cure_lasttime".format(i) for i in entities]

        # 查询疾病的治愈概率
        elif question_type == 'disease_cureprob':
            sql = ["MATCH (m:Disease) where m.name = '{0}' return m.name, m.cured_prob".format(i) for i in entities]

        # 查询疾病的治疗方式
        elif question_type == 'disease_cureway':
            sql = ["MATCH (m:Disease) where m.name = '{0}' return m.name, m.cure_way".format(i) for i in entities]

        # 查询疾病的易发人群
        elif question_type == 'disease_easyget':
            sql = ["MATCH (m:Disease) where m.name = '{0}' return m.name, m.easy_get".format(i) for i in entities]

        # 查询疾病的相关介绍
        elif question_type == 'disease_desc':
            sql = ["MATCH (m:Disease) where m.name = '{0}' return m.name, m.desc".format(i) for i in entities]

        # 查询疾病有哪些症状
        elif question_type == 'disease_symptom':
            sql = ["MATCH (m:Disease)-[r:has_symptom]->(n:Symptom) where m.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]

        # 查询症状会导致哪些疾病
        elif question_type == 'symptom_disease':
            sql = ["MATCH (m:Disease)-[r:has_symptom]->(n:Symptom) where n.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]

        # 查询疾病的并发症
        elif question_type == 'disease_acompany':
            sql1 = ["MATCH (m:Disease)-[r:acompany_with]->(n:Disease) where m.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]
            sql2 = ["MATCH (m:Disease)-[r:acompany_with]->(n:Disease) where n.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]
            sql = sql1 + sql2
        # 查询疾病的忌口
        elif question_type == 'disease_not_food':
            sql = ["MATCH (m:Disease)-[r:no_eat]->(n:Food) where m.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]

        # 查询疾病建议吃的东西
        elif question_type == 'disease_do_food':
            sql1 = ["MATCH (m:Disease)-[r:do_eat]->(n:Food) where m.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]
            sql2 = ["MATCH (m:Disease)-[r:recommand_eat]->(n:Food) where m.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]
            sql = sql1 + sql2

        # 已知忌口查疾病
        elif question_type == 'food_not_disease':
            sql = ["MATCH (m:Disease)-[r:no_eat]->(n:Food) where n.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]

        # 已知推荐查疾病
        elif question_type == 'food_do_disease':
            sql1 = ["MATCH (m:Disease)-[r:do_eat]->(n:Food) where n.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]
            sql2 = ["MATCH (m:Disease)-[r:recommand_eat]->(n:Food) where n.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]
            sql = sql1 + sql2

        # 查询疾病常用药品-药品别名记得扩充
        elif question_type == 'disease_drug':
            sql1 = ["MATCH (m:Disease)-[r:common_drug]->(n:Drug) where m.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]
            sql2 = ["MATCH (m:Disease)-[r:recommand_drug]->(n:Drug) where m.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]
            sql = sql1 + sql2

        # 已知药品查询能够治疗的疾病
        elif question_type == 'drug_disease':
            sql1 = ["MATCH (m:Disease)-[r:common_drug]->(n:Drug) where n.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]
            sql2 = ["MATCH (m:Disease)-[r:recommand_drug]->(n:Drug) where n.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]
            sql = sql1 + sql2
        # 查询疾病应该进行的检查
        elif question_type == 'disease_check':
            sql = ["MATCH (m:Disease)-[r:need_check]->(n:Check) where m.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]

        # 已知检查查询疾病
        elif question_type == 'check_disease':
            sql = ["MATCH (m:Disease)-[r:need_check]->(n:Check) where n.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]

        return sql



if __name__ == '__main__':
    handler = QuestionPaser()

3、问答程序脚本

from py2neo import Graph

class AnswerSearcher:
    def __init__(self):
        self.g = Graph("neo4j://192.168.112.30:7687", auth=("neo4j", "neo4jpassword"))
        self.num_limit = 20

    '''执行cypher查询,并返回相应结果'''
    def search_main(self, sqls):
        final_answers = []
        for sql_ in sqls:
            question_type = sql_['question_type']
            queries = sql_['sql']
            answers = []
            for query in queries:
                ress = self.g.run(query).data()
                answers += ress
            final_answer = self.answer_prettify(question_type, answers)
            if final_answer:
                final_answers.append(final_answer)
        return final_answers

    '''根据对应的qustion_type,调用相应的回复模板'''
    def answer_prettify(self, question_type, answers):
        final_answer = []
        if not answers:
            return ''
        if question_type == 'disease_symptom':
            desc = [i['n.name'] for i in answers]
            subject = answers[0]['m.name']
            final_answer = '{0}的症状包括:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'symptom_disease':
            desc = [i['m.name'] for i in answers]
            subject = answers[0]['n.name']
            final_answer = '症状{0}可能染上的疾病有:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_cause':
            desc = [i['m.cause'] for i in answers]
            subject = answers[0]['m.name']
            final_answer = '{0}可能的成因有:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_prevent':
            desc = [i['m.prevent'] for i in answers]
            subject = answers[0]['m.name']
            final_answer = '{0}的预防措施包括:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_lasttime':
            desc = [i['m.cure_lasttime'] for i in answers]
            subject = answers[0]['m.name']
            final_answer = '{0}治疗可能持续的周期为:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_cureway':
            desc = [';'.join(i['m.cure_way']) for i in answers]
            subject = answers[0]['m.name']
            final_answer = '{0}可以尝试如下治疗:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_cureprob':
            desc = [i['m.cured_prob'] for i in answers]
            subject = answers[0]['m.name']
            final_answer = '{0}治愈的概率为(仅供参考):{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_easyget':
            desc = [i['m.easy_get'] for i in answers]
            subject = answers[0]['m.name']

            final_answer = '{0}的易感人群包括:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_desc':
            desc = [i['m.desc'] for i in answers]
            subject = answers[0]['m.name']
            final_answer = '{0},熟悉一下:{1}'.format(subject,  ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_acompany':
            desc1 = [i['n.name'] for i in answers]
            desc2 = [i['m.name'] for i in answers]
            subject = answers[0]['m.name']
            desc = [i for i in desc1 + desc2 if i != subject]
            final_answer = '{0}的症状包括:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_not_food':
            desc = [i['n.name'] for i in answers]
            subject = answers[0]['m.name']
            final_answer = '{0}忌食的食物包括有:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_do_food':
            do_desc = [i['n.name'] for i in answers if i['r.name'] == '宜吃']
            recommand_desc = [i['n.name'] for i in answers if i['r.name'] == '推荐食谱']
            subject = answers[0]['m.name']
            final_answer = '{0}宜食的食物包括有:{1}\n推荐食谱包括有:{2}'.format(subject, ';'.join(list(set(do_desc))[:self.num_limit]), ';'.join(list(set(recommand_desc))[:self.num_limit]))

        elif question_type == 'food_not_disease':
            desc = [i['m.name'] for i in answers]
            subject = answers[0]['n.name']
            final_answer = '患有{0}的人最好不要吃{1}'.format(';'.join(list(set(desc))[:self.num_limit]), subject)

        elif question_type == 'food_do_disease':
            desc = [i['m.name'] for i in answers]
            subject = answers[0]['n.name']
            final_answer = '患有{0}的人建议多试试{1}'.format(';'.join(list(set(desc))[:self.num_limit]), subject)

        elif question_type == 'disease_drug':
            desc = [i['n.name'] for i in answers]
            subject = answers[0]['m.name']
            final_answer = '{0}通常的使用的药品包括:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'drug_disease':
            desc = [i['m.name'] for i in answers]
            subject = answers[0]['n.name']
            final_answer = '{0}主治的疾病有{1},可以试试'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'disease_check':
            desc = [i['n.name'] for i in answers]
            subject = answers[0]['m.name']
            final_answer = '{0}通常可以通过以下方式检查出来:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        elif question_type == 'check_disease':
            desc = [i['m.name'] for i in answers]
            subject = answers[0]['n.name']
            final_answer = '通常可以通过{0}检查出来的疾病有{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        return final_answer


if __name__ == '__main__':
    searcher = AnswerSearcher()

4、问答系统实现

4.1、模型初始化

from answer_search import *
from question_classifier import *
from question_parser import *
 
 
class ChatBotGraph:
    def __init__(self):
        self.classifier = QuestionClassifier()
        self.parser = QuestionPaser()
        self.searcher = AnswerSearcher()

4.2、问答主函数

    def chat_main(self, sent):
        answer = '您好,我是医药智能助理,希望可以帮到您。如果没答上来,可联系https://liuhuanyong.github.io/。祝您身体棒棒!'
        res_classify = self.classifier.classify(sent)
        if not res_classify:
            return answer
        res_sql = self.parser.parser_main(res_classify)
        final_answers = self.searcher.search_main(res_sql)
        if not final_answers:
            return answer
        else:
            return '\n'.join(final_answers)

4.3、运行主入口

运行 chatbot_graph.py 文件

if __name__ == '__main__':
    handler = ChatBotGraph()
    while 1:
        question = input('用户:')
        answer = handler.chat_main(question)
        print('医药智能助理:', answer)

4.4、运行结果

image-20241218113923894


一、写在开头

在OSI七层协议模型中应用层是距离我们最近,且日后开发使用到最多的一层,在上一篇博文中我们已经学习了应用层中的HTTP协议,在本文中我们再一起来学一下DNS。啥?DNS不是很了解?那还不快往下看!

其实早几年和一个朋友聊天,当时,他是一个网络条线的技术经理,向我吐槽说,现在来面试的很多都是学不好编程语言的,转来搞网络了,结果问一个DNS,他都整不明白,就这还好意思干网络?我当时吓得不敢说话,因为我自己对于网络编程也是那种浅尝辄止的心态,生怕他回头问我一个问题,哈哈。


二、DNS的定义

DNS(Domain Name System)域名管理系统,是当用户使用浏览器访问网址之后,使用的第一个重要协议。DNS 要解决的是域名和 IP 地址的映射问题。

image




DNS是一个联机的分布式数据库系统,它具有分布式、层次化、模块化等特点,它属于应用层的协议,基于UDP传输,端口为53。


三、DNS服务器类别

我们上面说到DNS具有层次化结构,而这个层次化主要体现在它的服务器部署之上,DNS的域名层次结构是一棵树,从

根域名服务器



顶级域名服务器



权威域名服务器

,再到

本地域名服务器

,而我们所有遇到的DNS服务器,都囊括在这四类之中。


  • 根域名DNS服务器

    :是为提供TLD服务器的IP地址,目前世界上仅有13组根服务器,我国境内仍然没有,不过后来任播应用后,DNS分解出1089个根域名服务器节点,而在我国便有了26个根域名服务器节点。

  • 顶级域名服务器(TLD服务器)

    :顶级域名指的域名的后缀,像com、org、net等都是,不同国家也有自己的域名,如uk、ca、fr这些,我国的是cn。而顶级域名系统则是提供权威DNS服务器IP地址的。

    image


  • 权威域名DNS服务器

    :所有在因特网上具有公共可访问主机的组织机构,必须提供可访问的DNS记录,而这些记录里则保存着域名与IP映射等信息,而这些记录则存储在权威DNS服务器上。

  • 本地域名服务器

    :每个 ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 层次结构中。

那么了解了DNS的底层服务器结构,它们是如何合作的,或者说是通过怎样的机制,达到解析域名,响应到对应的IP上的呢?继续往下看!


四、DNS的解析过程

开局先上一张图:

image



现在假设我们用浏览器去访问“
www.baidu.com.cn” 的IP地址,那么DNS解析系统的整个解析过程大致可以分为如下这几步
:

  1. 第一步:客户端向本地DNS服务器发送DNS请求报文,报文中包含域名 www.baidu.com.cn ,若本地DNS中有相应的记录,直接反馈客户端,若没有则访问根DNS服务器,继续处理请求;
  2. 第二步:本地DNS服务器向根域名服务器发送请求,根域名服务器是域名系统的最顶层,它负责管理所有顶级域名(如.com、.net、.org以及国家和地区代码如.cn)。根域名服务器会返回所查询域(在这个例子中是“.cn”域)的主域名服务器的地址;
  3. 第三步:本地DNS在接收到根服务器返回的地址后,便向cn顶级域名服务器发送请求,一般到这里是有可能找到了对应的域名映射的IP地址的,但若还没有,它会查询自己的记录以找到“.com.cn”域的主域名服务器(权威DNS服务器)地址返回;
  4. 第四步:本地DNS服务器只得继续向权威DNS服务器发去请求,终于,
    www.baidu.com.cn已经向权威域名DNS备案过了,在这里找到了它对应的记录,便把记录返回给本地DNS;
  5. 第五步:本地dns服务器将收到的返回地址发给客户端,同时写入自己的缓存,以便下次查询;


需要注意的是:

从请求主机到本地 DNS 服务器的查询是递归的,其余的查询时迭代的;当找不到相应记录,会返回空结果,而不是超时信息。


五、DNS报文格式

DNS报文格式是DNS协议中用于客户端和服务器之间通信的一种结构化数据格式。DNS报文主要分为两种:查询报文和回答报文,它们具有相同的格式。

image




DNS(Domain Name System,域名系统)报文是在域名解析过程中,DNS客户端和DNS服务器之间传输的消息,其报文格式主要包括以下几个部分:


我们可以将其分成三个部分来看:

报文头部


  • 标识

    :16位的标识符,用于标识该DNS报文,客户端发送请求时会生成一个随机的标识,服务器在响应时会使用相同的标识,以便客户端能够将响应与请求对应起来。

  • 标志

    :16位的标志字段,包含多个标志位,用于表示报文的类型、操作码、响应状态等信息。例如,QR位表示是查询报文(0)还是响应报文(1);OPCODE位表示操作码,通常为标准查询(0)等。

  • 问题数量

    :16位的字段,指明报文中包含的问题记录数量。

  • 回答数量

    :16位的字段,指明报文中包含的回答记录数量。

  • 权威机构数量

    :16位的字段,指明报文中包含的权威机构记录数量。

  • 附加信息数量

    :16位的字段,指明报文中包含的附加信息记录数量。

问题部分


  • 查询名称

    :可变长度字段,包含要查询的域名,域名采用一种特殊的编码方式,将域名的各个部分以标签的形式表示,每个标签前面有一个字节表示该标签的长度,最后以一个字节的0结束。

  • 查询类型

    :16位的字段,指定查询的资源记录类型,如A记录(表示主机地址)、NS记录(表示域名服务器)、CNAME记录(表示别名)等。

  • 查询类

    :通常为1,表示Internet类。

回答部分、权威机构部分和附加信息部分

这三个部分的格式基本相同,每个部分都可以包含多个资源记录,每个资源记录的格式如下:


  • 域名

    :与问题部分的查询名称格式相同,通常是被查询域名或相关域名。

  • 类型

    :16位的字段,与问题部分的查询类型相对应,表示该资源记录的类型。



  • :通常为1,表示Internet类。

  • 生存时间

    :32位的字段,以秒为单位,表示该资源记录在缓存中的有效时间。

  • 资源数据长度

    :16位的字段,指明后面的资源数据的长度。

  • 资源数据

    :可变长度字段,包含与该资源记录类型相关的具体数据,如A记录的IP地址、NS记录的域名服务器名称等。


总结

以上就是DNS域名管理系统涉及到的知识点啦,虽然很多后端程序员吗,在日后的工作中几乎不再深追DNS的底层原理,但初期,我们还是有必要了解一些网络上的内容的,毕竟企业内转岗比较常见。