百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

深入理解子进程Python 相关源码解析

toyiye 2024-06-21 12:25 11 浏览 0 评论

很多时候,我需要写脚本去做一些自动化操作,简单的可以直接写 Shell 脚本,但一些稍复杂的情况, 比如要用到分支语句,循环语句,或者调用一些高级函数,用 Shell 就太费劲了。 我更喜欢用一种完整的语言(比如 Python),调用 Shell 程序并获取它的输出,执行复杂操作。

本文介绍 UNIX 进程的创建过程(fork, exec),如何与子进程通信(pipe, pty), 并深入分析标准库中的 subprocess 模块和著名的第三方库 sh 的源码,阐述其实现原理。

这些库提供了非常易用的接口,大部分时候只要看看文档就能解决问题。 但它们也无法彻底掩盖实现细节,The Law of Leaky Abstractions - 抽象漏洞法则 解释了许多不完美的抽象,希望这篇文章也能帮你更好地理解子进程。

UNIX 进程的创建过程

fork

fork 函数以复制调用进程的方式创建新进程,它 调用一次,返回两次,在子进程中它返回0, 在父进程中它返回子进程的 ID。

  1. import os

  2. def test_fork:

  3. pid = os.fork

  4. if pid == 0:

  5. print('child')

  6. os._exit(0)

  7. else:

  8. print('parent')

  9. test_fork

fork 之后父子进程各自继续往下执行,所以示例中的两个 print都会执行, 子进程通过os._exit退出,否则两个进程共用标准输入,shell 就不能正常工作了。

fork是个系统调用,可通过man fork.2查看其手册。

_exit也是个系统调用,它与sys.exit的区别在于:_exit直接退出, 而sys.exit先执行一些清理工作再退出。可通过man exit.2man exit.3查看其手册。

exec

exec 是一系列函数,总共有七个,它把当前进程执行的程序替换成新程序,正常情况下它们 调用一次,永不退出

  1. import os

  2. os.execv('/bin/sh', ['sh'])

执行示例中的代码之后,进程就变成了 sh,无法再回到 python。

exec 函数中,通常 execve 是系统调用,其他几个都能通过它来实现, 可通过 man execve.2man exec.3查看其手册。

waitpid

通常 fork 之后,子进程调用 exec 执行新程序,父进程要等待子进程的结果, 这就可以通过 waitpid 实现。如果父进程没有调用 waitpid 获取子进程的退出状态, 那么子进程退出后,状态信息会一直保留在内存中,这样的子进程也被称为僵尸进程。

  1. import os

  2. def system(cmd):

  3. pid = os.fork

  4. if pid == 0:

  5. os.execv('/bin/sh', ['sh', '-c', cmd])

  6. else:

  7. return os.waitpid(pid, 0)

  8. system('python --version')

这就是 os.system的实现原理,子进程共用父进程的标准输入输出,父进程阻塞,直到子进程结束。 因为子进程的输出是直接送到标准输出,所以无法被父进程获取。

可通过 man waitpid.2man system.3查看其手册。

dup2

  1. int dup2(int oldfd, int newfd);

dup2 可以复制文件描述符, 它会先把 newfd 关闭,再把 oldfd 复制到 newfd, 经常用它来修改进程的标准 I/O。

  1. import os, sys

  2. out = open('out.txt', 'w')

  3. os.dup2(out.fileno, sys.stdout.fileno)

  4. print('这段文字会输出到 out.txt 中,而不是控制台')

可通过 man dup2.2查看手册。

进程间通信

管道

进程之间有很多种通信方式,这里只讨论 管道这种方式,这也是最常用的一种方式。 管道通常是半双工的,只能一端读,另一端写,为了可移植性,不能预先假定系统支持全双工管道。

  1. import os

  2. r, w = os.pipe

  3. os.write(w, b'hello')

  4. print(os.read(r, 10))

可通过 man pipe.2查看其手册。

缓冲 I/O

I/O 可分为:无缓冲行缓冲全缓冲三种。

通过 read 和 write 系统调用直接读写文件,就是无缓冲模式,性能也最差。 而通过标准 I/O 库读写文件,就是缓冲模式,标准 I/O 库提供缓冲的目的是尽可能减少 read 和 write 调用的次数,提高性能。

行缓冲模式,当在输入输出中遇到换行符时,才进行实际 I/O 操作。 全缓冲模式,当填满缓冲区时,才进行实际 I/O 操作。

管道和普通文件默认是全缓冲的,标准输入和标准输出默认是行缓冲的,标准错误默认是无缓冲的。

  1. import os

  2. r, w = os.pipe

  3. fw = os.fdopen(w, 'wb')

  4. fr = os.fdopen(r, 'rb')

  5. fw.write(b'hello')

  6. print(fr.read)

这个例子中,读管道这步会一直阻塞。有两个原因:

  1. 写管道有缓冲,没有进行实际 I/O,所以读端读不到数据

  2. 读管道也有缓冲,要读满缓冲区才会返回

只要满足其中任何一个条件都会阻塞。通常写管道是在子进程中进行,我们无法控制其缓冲, 这也是管道的一个局限性。

伪终端

伪终端看起来就像一个双向管道,一端称为 master(主),另一端称为 slave(从)。 从端看上去和真实的终端一样,能够处理所有的终端 I/O 函数。

  1. import os

  2. master, slave = os.openpty

  3. os.write(master, b'hello from mastern')

  4. print(os.read(slave, 50))

  5. # b'hello from mastern'

  6. os.write(slave, b'hello from slaven')

  7. print(os.read(master, 50))

  8. # b'hello from masterrnhello from slavern'

伪终端 echo 默认是开启的,所以最后 master 读的时候,会先读出之前写入的数据。

如果写入的数据没有换行符,就可能不会被传送到另一端, 造成读端一直阻塞(猜测是伪终端的底层实现使用了行缓冲)。

可通过 man openpty.3man pty.7查看其手册。

subprocess 的实现

subprocess 提供了很多接口, 其核心是 Popen(Process Open),其他部分都通过它来实现。subprocess 也支持 Windows 平台, 这里只分析它在 Unix 平台的实现。

subprocess 源码: https://github.com/python/cpython/blob/3.6/Lib/subprocess.py#L540

首先看一下接口原型(L586),参数非常多:

  1. def __init__(self, args, bufsize=-1, executable=None,

  2. stdin=None, stdout=None, stderr=None,

  3. preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS,

  4. shell=False, cwd=None, env=None, universal_newlines=False,

  5. startupinfo=None, creationflags=0,

  6. restore_signals=True, start_new_session=False,

  7. pass_fds=, *, encoding=None, errors=None):

  8. """Create new Popen instance."""

然后看到中间(L648) 一大段注释:

  1. # Input and output objects. The general principle is like

  2. # this:

  3. #

  4. # Parent Child

  5. # ------ -----

  6. # p2cwrite ---stdin---> p2cread

  7. # c2pread <--stdout--- c2pwrite

  8. # errread <--stderr--- errwrite

  9. #

  10. # On POSIX, the child objects are file descriptors. On

  11. # Windows, these are Windows file handles. The parent objects

  12. # are file descriptors on both platforms. The parent objects

  13. # are -1 when not using PIPEs. The child objects are -1

  14. # when not redirecting.

  15. (p2cread, p2cwrite,

  16. c2pread, c2pwrite,

  17. errread, errwrite) = self._get_handles(stdin, stdout, stderr)

父子进程通过这三对文件描述符进行通信。

再跳到 _get_handles的实现(L1144):

  1. def _get_handles(self, stdin, stdout, stderr):

  2. """Construct and return tuple with IO objects:

  3. p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite

  4. """

  5. p2cread, p2cwrite = -1, -1

  6. c2pread, c2pwrite = -1, -1

  7. errread, errwrite = -1, -1

  8. if stdin is None:

  9. pass

  10. elif stdin == PIPE:

  11. p2cread, p2cwrite = os.pipe

  12. elif stdin == DEV:

  13. p2cread = self._get_dev

  14. elif isinstance(stdin, int):

  15. p2cread = stdin

  16. else:

  17. # Assuming file-like object

  18. p2cread = stdin.fileno

它创建了三对文件描述符, stdinstdoutstderr这三个参数都是类似的, 分别对应一对文件描述符。当参数等于PIPE时,它就会创建一条管道,这也是最常用的参数。

回到 Popen,接着往下看(L684):

  1. if p2cwrite != -1:

  2. self.stdin = io.open(p2cwrite, 'wb', bufsize)

  3. if text_mode:

  4. self.stdin = io.TextIOWrapper(self.stdin, write_through=True,

  5. line_buffering=(bufsize == 1),

  6. encoding=encoding, errors=errors)

  7. if c2pread != -1:

  8. self.stdout = io.open(c2pread, 'rb', bufsize)

  9. if text_mode:

  10. self.stdout = io.TextIOWrapper(self.stdout,

  11. encoding=encoding, errors=errors)

  12. if errread != -1:

  13. self.stderr = io.open(errread, 'rb', bufsize)

  14. if text_mode:

  15. self.stderr = io.TextIOWrapper(self.stderr,

  16. encoding=encoding, errors=errors)

  17. self._execute_child(args, executable, preexec_fn, close_fds,

  18. pass_fds, cwd, env,

  19. startupinfo, creationflags, shell,

  20. p2cread, p2cwrite,

  21. c2pread, c2pwrite,

  22. errread, errwrite,

  23. restore_signals, start_new_session)

它把其中三个文件描述符变成了文件对象,用于在父进程中和子进程通信。 注意 bufsize参数只作用于父进程中的文件对象,子进程中的缓冲是没法控制的。

encodingerrors参数就不赘述了,可以查看 io 模块的文档。

再看 _execute_child的实现(L1198):

  1. def _execute_child(self, args, executable, preexec_fn, close_fds,

  2. pass_fds, cwd, env,

  3. startupinfo, creationflags, shell,

  4. p2cread, p2cwrite,

  5. c2pread, c2pwrite,

  6. errread, errwrite,

  7. restore_signals, start_new_session):

  8. """Execute program (POSIX version)"""

  9. if isinstance(args, (str, bytes)):

  10. args = [args]

  11. else:

  12. args = list(args)

  13. if shell:

  14. args = ["/bin/sh", "-c"] args

  15. if executable:

  16. args[0] = executable

看到这里, args,shell,executable这几个参数就很好理解了。

args可以是字符串或列表,shell表示通过 shell 程序执行命令,executable是 shell 程序的路径,默认是/bin/sh

args是字符串时,通常配合shell=True使用,用来执行任意 shell 命令。

接着往下看(L1221):

  1. # For transferring possible exec failure from child to parent.

  2. # Data format: "exception name:hex errno:description"

  3. # Pickle is not used; it is complex and involves memory allocation.

  4. errpipe_read, errpipe_write = os.pipe

这是第四对文件描述符。子进程在 fork 之后到 exec 之前会执行许多步骤, 万一这些步骤失败了,就通过这对文件描述向父进程传递异常信息,父进程收到后抛出相应的异常。

创建子进程并执行命令(L1252):

  1. fds_to_keep = set(pass_fds)

  2. fds_to_keep.add(errpipe_write)

  3. self.pid = _posixsubprocess.fork_exec(

  4. args, executable_list,

  5. close_fds, sorted(fds_to_keep), cwd, env_list,

  6. p2cread, p2cwrite, c2pread, c2pwrite,

  7. errread, errwrite,

  8. errpipe_read, errpipe_write,

  9. restore_signals, start_new_session, preexec_fn)

  10. self._child_created = True

继续追踪 _posixsubprocess源码: https://github.com/python/cpython/blob/3.6/Modules/_posixsubprocess.c#L545

  1. static PyObject *

  2. subprocess_fork_exec(PyObject* self, PyObject *args){

  3. // ...省略一堆处理参数的代码

  4. pid = fork;

  5. if (pid == 0) {

  6. // L692

  7. child_exec(exec_array, argv, envp, cwd,

  8. p2cread, p2cwrite, c2pread, c2pwrite,

  9. errread, errwrite, errpipe_read, errpipe_write,

  10. close_fds, restore_signals, call_setsid,

  11. py_fds_to_keep, preexec_fn, preexec_fn_args_tuple);

  12. }

  13. // L733

  14. return PyLong_FromPid(pid);

在子进程中执行 child_exec,父进程返回 pid。

继续看 child_exec 的实现(L390):

  1. static void

  2. child_exec(/*一堆参数*/)

  3. {

  4. int i, saved_errno, reached_preexec = 0;

  5. PyObject *result;

  6. const char* err_msg = "";

  7. /* Buffer large enough to hold a hex integer. We can't malloc. */

  8. char hex_errno[sizeof(saved_errno)*2 1];

  9. if (make_inheritable(py_fds_to_keep, errpipe_write) < 0)

  10. goto error;

make_inheritable把 Popen 中pass_fds参数指定的文件描述符设为可继承, 它通过对每个文件描述符调用_Py_set_inheritable,清除close-on-exec标志。

需要注意,父进程的标准 I/O 默认是可继承的。

参考 PEP 446 – Make newly created file descriptors non-inheritable

  1. int flags, res;

  2. flags = fcntl(fd, F_GETFD);

  3. if (flags == -1) { /* handle the error */ }

  4. flags |= FD_CLOEXEC;

  5. /* or "flags &= ~FD_CLOEXEC;" to clear the flag */

  6. res = fcntl(fd, F_SETFD, flags);

  7. if (res == -1) { /* handle the error */ }

close-on-exec(FD_CLOEXEC) 标志表示这个文件会在进程执行 exec 系统调用时自动关闭。

接着往下看(L430):

  1. /* Dup fds for child.

  2. dup2 removes the CLOEXEC flag but we must do it ourselves if dup2

  3. would be a no-op (issue #10806). */

  4. if (p2cread == 0) {

  5. if (_Py_set_inheritable(p2cread, 1, ) < 0)

  6. goto error;

  7. }

  8. else if (p2cread != -1)

  9. POSIX_CALL(dup2(p2cread, 0)); /* stdin */

通过 dup2 系统调用,把 p2cread 设为子进程的标准输入。标准输出和标准错误也是类似的。

接着往下看(L463):

  1. if (cwd)

  2. POSIX_CALL(chdir(cwd));

  3. if (restore_signals)

  4. _Py_RestoreSignals;

  5. #ifdef HAVE_SETSID

  6. if (call_setsid)

  7. POSIX_CALL(setsid);

  8. #endif

cwd参数,设置当前工作目录。

restore_signals参数,把信号处理恢复为默认值,涉及信号处理的内容,这里略过。

call_setsid,即 Popen 的start_new_session参数, 创建会话并设置进程组 ID,内容太多也略过。

接着往下看(L474):

  1. reached_preexec = 1;

  2. if (preexec_fn != Py_None && preexec_fn_args_tuple) {

  3. /* This is where the user has asked us to deadlock their program. */

  4. result = PyObject_Call(preexec_fn, preexec_fn_args_tuple, );

  5. if (result == ) {

  6. /* Stringifying the exception or traceback would involve

  7. * memory allocation and thus potential for deadlock.

  8. * We've already faced potential deadlock by calling back

  9. * into Python in the first place, so it probably doesn't

  10. * matter but we avoid it to minimize the possibility. */

  11. err_msg = "Exception occurred in preexec_fn.";

  12. errno = 0; /* We don't want to report an OSError. */

  13. goto error;

  14. }

  15. /* Py_DECREF(result); - We're about to exec so why bother? */

  16. }

  17. /* close FDs after executing preexec_fn, which might open FDs */

  18. if (close_fds) {

  19. /* TODO HP-UX could use pstat_getproc if anyone cares about it. */

  20. _close_open_fds(3, py_fds_to_keep);

  21. }

执行 preexec_fn,随后根据close_fds参数判断是否关闭打开的文件描述符。

最后,执行命令(L497):

  1. /* This loop matches the Lib/os.py _execvpe's PATH search when */

  2. /* given the executable_list generated by Lib/subprocess.py. */

  3. saved_errno = 0;

  4. for (i = 0; exec_array[i] != ; i) {

  5. const char *executable = exec_array[i];

  6. if (envp) {

  7. execve(executable, argv, envp);

  8. } else {

  9. execv(executable, argv);

  10. }

  11. if (errno != ENOENT && errno != ENOTDIR && saved_errno == 0) {

  12. saved_errno = errno;

  13. }

  14. }

尝试执行命令,只要有一个成功,后面的就不会执行了,因为 exec 执行成功后永不返回。

Popen 的创建过程到这就结束了,子进程已经运行起来了,接下来分析如何与子进程通信。

跳到 Popen 中的 communicate 方法(L796):

  1. def communicate(self, input=None, timeout=None):

  2. if self._communication_started and input:

  3. raise ValueError("Cannot send input after starting communication")

这个 raise ValueError是为什么呢?稍后解答。

继续往下看(L813):

  1. # Optimization: If we are not worried about timeouts, we haven't

  2. # started communicating, and we have one or zero pipes, using select

  3. # or threads is unnecessary.

  4. if (timeout is None and not self._communication_started and

  5. [self.stdin, self.stdout, self.stderr].count(None) >= 2):

  6. stdout = None

  7. stderr = None

  8. if self.stdin:

  9. self._stdin_write(input)

  10. elif self.stdout:

  11. stdout = self.stdout.read

  12. self.stdout.close

  13. elif self.stderr:

  14. stderr = self.stderr.read

  15. self.stderr.close

  16. self.wait

  17. else:

  18. if timeout is not None:

  19. endtime = _time timeout

  20. else:

  21. endtime = None

  22. try:

  23. stdout, stderr = self._communicate(input, endtime, timeout)

  24. finally:

  25. self._communication_started = True

  26. sts = self.wait(timeout=self._remaining_time(endtime))

  27. return (stdout, stderr)

if部分直接和子进程通信,else部分调用了self._communicate, 用线程和 I/O 多路复用的方式与子进程通信,去掉影响也不大,这部分细节就不分析了。 注意self._stdin_write(input)很关键,在self._communicate中也是调用它向子进程写数据。

_stdin_write的实现(L773):

  1. def _stdin_write(self, input):

  2. # L776

  3. self.stdin.write(input)

  4. # L787

  5. self.stdin.close

省略了异常处理的代码,主要就这两句。可以看到写完输入之后,立即把标准输入关闭了。 结合上面的 raise ValueError,可以看出只能向子进程写入一次。

这涉及到缓冲的问题,管道是默认全缓冲的,如果不立即关闭写端,子进程读的时候就可能一直阻塞, 我们没法控制子进程使用什么类型的缓冲。同样的,如果子进程一直写输出,父进程也会读不到数据, 只有子进程退出之后,系统自动关闭它的标准输出,父进程才能读到数据。

下面是个例子:

  1. # hello.py

  2. from time import sleep

  3. while True:

  4. print('hello world')

  5. sleep(1)

  6. # test_hello.py

  7. from subprocess import *

  8. p = Popen(['python', 'hello.py'], stdin=PIPE, stdout=PIPE)

  9. out, err = p.communicate

  10. print(out)

运行 python test_hello.py,尽管子进程在不停地输出,但是父进程一直读取不到数据。 此时如果我们把子进程杀死,父进程便会立即读到数据。

subprocess 的核心代码到这就分析完了。 再放个自己实现的 popen(未考虑各种异常情况,仅用于说明实现原理):

  1. # popen.py

  2. import os

  3. class popen:

  4. def __init__(self, cmd, *, cwd=None, env=None, input=None):

  5. argv = ['/bin/sh', '-c', cmd]

  6. # Parent Child

  7. # ------ -----

  8. # p2cwrite ---stdin---> p2cread

  9. # c2pread <--stdout--- c2pwrite

  10. # errread <--stderr--- errwrite

  11. p2cread, p2cwrite = os.pipe

  12. c2pread, c2pwrite = os.pipe

  13. errread, errwrite = os.pipe

  14. pid = os.fork

  15. if pid == 0:

  16. os.close(p2cwrite)

  17. os.close(c2pread)

  18. os.close(errread)

  19. os.dup2(p2cread, 0)

  20. os.dup2(c2pwrite, 1)

  21. os.dup2(errwrite, 2)

  22. if cwd:

  23. os.chdir(cwd)

  24. if env:

  25. os.execve('/bin/sh', argv, env)

  26. else:

  27. os.execv('/bin/sh', argv)

  28. else:

  29. os.close(p2cread)

  30. os.close(c2pwrite)

  31. os.close(errwrite)

  32. stdin = open(p2cwrite, 'w')

  33. stdout = open(c2pread, 'r')

  34. stderr = open(errread, 'r')

  35. # communicate

  36. if input:

  37. stdin.write(input)

  38. stdin.close

  39. self.out = stdout.read

  40. stdout.close

  41. self.err = stderr.read

  42. stderr.read

  43. self.status = os.waitpid(pid, 0)[1]

  44. def __repr__(self):

  45. fmt = ('STATUS:{}n'

  46. 'OUTPUT'.center(80, '-') 'n{}n'

  47. 'ERROR'.center(80, '-') 'n{}n')

  48. return fmt.format(self.status, self.out, self.err)

执行命令试一试:

  1. >>> from popen import popen

  2. >>> popen('python -c "import this"')

  3. STATUS:0

  4. -------------------------------------OUTPUT-------------------------------------

  5. The Zen of Python, by Tim Peters

  6. Beautiful is better than ugly.

  7. Explicit is better than implicit.

  8. Simple is better than complex.

  9. Complex is better than complicated.

  10. Flat is better than nested.

  11. Sparse is better than dense.

  12. Readability counts.

  13. Special cases aren't special enough to break the rules.

  14. Although practicality beats purity.

  15. Errors should never pass silently.

  16. Unless explicitly silenced.

  17. In the face of ambiguity, refuse the temptation to guess.

  18. There should be one-- and preferably only one --obvious way to do it.

  19. Although that way may not be obvious at first unless you're Dutch.

  20. Now is better than never.

  21. Although never is often better than *right* now.

  22. If the implementation is hard to explain, it's a bad idea.

  23. If the implementation is easy to explain, it may be a good idea.

  24. Namespaces are one honking great idea -- let's do more of those!

  25. -------------------------------------ERROR--------------------------------------

  26. >>> popen('python -c "import that"')

  27. STATUS:256

  28. -------------------------------------OUTPUT-------------------------------------

  29. -------------------------------------ERROR--------------------------------------

  30. Traceback (most recent call last):

  31. File "<string>", line 1, in <module>

  32. ModuleNotFoundError: No module named 'that'

  33. >>> popen('python --version').out

  34. 'Python 3.6.1n'

  35. >>>

题图:pexels,CC0 授权。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码