Zan*_*nna 6 command-line bash scripts
这个问题是由我在 Linux 杂志中找到的一个简短脚本提示的。为了证明我没有编造这个,这是一张照片:
我想给这个出版物的编辑写一封信,说明这有什么问题以及如何写得更好。
该脚本尝试将 jpeg 文件捕获到一个变量中,以便lepton可以对它们进行某些操作(使用 压缩)。
for jpeg in `echo "$(file $(find ./ ) |
grep JPEG | cut -f 1 -d ':')"`
do
/path/to/command "$jpeg"
...
Run Code Online (Sandbox Code Playgroud)
显然,在这种情况下,我们不能相信以.jpg扩展名命名的文件,所以我们不能用类似的东西来捕捉它们
for f in *.JPG *.jpg *.JPEG *.jpeg ; do ...
Run Code Online (Sandbox Code Playgroud)
因为作者使用了file检查他们的类型,但如果其文件名不能被信任有一个合理的扩展,那么我看不出我们如何能相信他们不会是-rf *或(; \ $!|或有换行符或任何其他。
如何通过使用foror类型将文件明智地捕获到变量中while,或者通过使用findwith-exec或其他一些方法来避免这样做?
深入了解和演示图片中的代码有什么问题的奖金。
我已经用 [bash] 标记了这个问题,因为它是关于 bash 脚本的,但是如果你想用一种不使用 bash 的方法来回答,那么请随意这样做。
您的问题中显示的脚本尝试枚举文件并检查它们是否为 JPEG,但两者都不可靠。它尝试所有的传递路径,以file在单次运行,并提取两个文件名称和类型从的输出file,这是合理的,因为它可以比运行速度更快file的每个文件连连。但是要正确执行此操作,您需要注意路径如何传递到file、如何file分隔其输出以及如何使用该输出。你可以使用这个:
#!/bin/bash
find . -exec file --mime-type -r0F '' {} + | while read -rd ''; do
read -r mimetype
case "$mimetype" in image/jpeg)
# Bash placed the filename in "$REPLY" -- put commands that use it here.
# You can have as many commands as you want before the closing ";;" token.
;;
esac
done
Run Code Online (Sandbox Code Playgroud)
这是几种正确的方法之一。(它并不需要设置IFS=;见下文)。find用+传球多路径参数file,只运行它多次作为必要处理它们,通常只有一次。信用去??s ? 对于传递--mime-type到file获取MIME类型的想法,其中包含您实际想要的信息并且易于解析。
详细说明如下。我以 JPEG 压缩的具体任务为例。这就是您展示的脚本的用途,并且lepton在决定如何改进该脚本时应该考虑一些奇怪之处。如果您只想查看lepton在每个 JPEG 文件上运行的脚本,您可以跳到第7节。将它们全部放在一起。
lepton您展示的脚本旨在遍历目录层次结构,查找 JPEG 图像,并使用无损 JPEG 压缩器处理它们lepton。对于您的问题的主要动机,命令可能并不重要,但不同的命令具有不同的语法。某些命令在一次运行中接受多个输入文件名。大多数接受--指示选项的结束。我会用lepton我的例子。该lepton命令不接受多个输入文件名并且不识别--.
要使用lepton,请先安装它。它已正式打包用于 Ubuntu 17.04 及更高版本( sudo apt install lepton)。对于较早的 Ubuntu 版本,或者要使用比为您的版本打包的更新版本,请克隆其git存储库( git clone https://github.com/dropbox/lepton.git) 并按照 README 中的说明构建源代码。或者您可以找到 PPA。
您可以根据自己安装它,lepton可能是/usr/bin,/usr/local/bin或其它地方。可能你会想要它在$PATH; 然后你可以运行它lepton。该脚本,你表现出用途的绝对路径,以lepton与标准工具mv和rm,但不会对其他标准工具file,find,grep和cut。(这是猛砸,所以echo--pointless在脚本anyway--是一个shell内置的,exit是始终是一个内置的)。虽然这不是剧本的一个严重缺陷,有没有明显的原因,这样的不一致。除非你正在编写一个脚本来容忍没有$PATH 明智地设置——在这种情况下,你必须对所有外部命令使用绝对路径——我建议对标准命令和你安装的命令使用相对路径。
lepton我用lepton v1.0-1.2.1-104-g209463a(来自Git)进行了测试。lepton于2016 年 7 月发布,所以我猜当前的语法将继续有效。但未来的版本可能会增加功能。如果您从现在开始阅读,您可能会检查是否lepton已经添加了对曾经需要脚本编写的任务的支持。
请注意您传递的命令行参数。例如,我尝试lepton以-verbose第一个参数和art.jpg第二个参数运行。它被解释-verbose为输入文件名并退出并出现错误,但在截断之前不会art.jpg- 它被解释为输出文件名 - 到零字节。幸好我有备份!
您可以将零个、一个或两个路径传递给lepton. 在所有情况下,它都会检查其输入文件或流以查看它是否包含 JPEG 或 Lepton 数据。JPEG 被压缩为 Lepton;Lepton 被解压缩为 JPEG。lepton将删除和添加文件扩展名,但不使用它们来决定要做什么。
lepton -从stdin读取并写入stdout。因此,这是读取和写入 的一种方式,即使它们的名称以(就像选项一样)开头。但是我将使用的方法传递以 开头的路径,所以我不必担心这个。lepton - < infile > outfileinfileoutfile-.
lepton infileinfile这就是您展示的脚本如何使用lepton.
如果内容infile看起来像JPEG,则lepton输出一个Lepton文件;如果它的内容看起来像一个 Lepton 文件,则lepton输出一个 JPEG。lepton通过从 中剥离扩展名infile(如果有)并根据它创建的文件类型添加 a.jpg或.lep扩展名来决定它想要如何命名其输出文件。但它并没有使用它删除扩展名(如果有的话)来推断它是经营上的文件类型。
它将最后一个 .及其后的任何内容视为扩展。如果infile是a.b.c,你得到a.b.lep或a.b.jpg。如果文件名以 a 开头而.没有其他.s,lepton 仍将其视为扩展名:从名为.abcyou get的 JPEG 中获取.lep。只有.在文件名中——而不是目录名——触发这个,所以从x/fo.o/abc你得到的 Lepton 文件x/fo.o/abc.jpg(你想要的),而不是x/fo.jpg(这会很糟糕)。
如果输出文件名取得这样的名字已经存在的文件,_分别追加到最后,后延,直到没有,并使用加下划线的名称:abc.lep,abc.lep_,abc.lep__,等xyz.jpg,xyz.jpg_,xyz.jpg__等。
自动删除和添加扩展名以及添加下划线避免了您必须自己管理的问题——在输出文件已经存在时防止数据丢失。但它也暴露了您展示的脚本中可能存在的深层设计缺陷。如果您的文件命名合理,那么您的所有 JPEG 文件都以.jpgor结尾.jpeg(可能大写),并且没有非 JPEG 文件如此命名。但是,您不必检查文件file以找出哪些是 JPEG!
因此,您展示的脚本的前提是文件可能没有合理命名。脚本在包含空格*、 和其他特殊字符的文件名上出现错误或意外行为总是不好的。因此,它在空格上拆分和扩展 glob 的行为(外部未加引号的命令替换,只是为了拆分单独的文件名,这样做)特别糟糕。有关详细信息,请参阅Byte Commander 的出色回答。这可能是您展示的脚本中最严重的缺陷。
但同样值得考虑的是,最后一个.在概念上没有以文件扩展名开头的文件名会发生什么。假设Pictures有四个文件,JPEG图片都:01. Milan wide-angle sunset,01. Milan wide-angle sunset highres,02. Kyle birthday party prep - blooper cakes,和03. The subtle found art of unopened expired paint cans with peeling labels。然后for f in ~/Pictures/0*; do lepton "$f"; done创建01.lep, 01.lep_,02.lep和 --03.lep可能不是您想要的。
如果您有未命名的 JPEG.jpg或JPEG .jpeg,最好的通用方法是以这种方式重命名它们并调查这样做时出现的任何命名冲突。但这超出了本答案的范围。
这些重命名问题发生在 JPEG 不像 JPEG 那样命名,而不是像 JPEG 那样命名的非 JPEG 中。但即便如此,也可能有更好的解决方案。如果问题是._来自 macOS 的文件并且您不想删除它们,只需排除带有前导._(甚至前导.)的文件。尽管如此,仅通过一条路径以lepton避免数据丢失(由于其_附加规则);如果主要目标是排除非 JPEG,即使实现需要修复,基本思想也是合理的。
所以我将使用单路径语法lepton infile。但是任何考虑lepton在奇怪命名的文件上进行这样的自动化的人都应该记住,生成的.lep文件可能以不显示输入文件名的方式命名。
lepton infile outfile但仅仅因为你期望它并不能使它成为正确的事情。
至于其他的方式来运行lepton,lepton决定是否infile要被压缩为JPEG或轻子文件通过检查其内容进行解压缩。如果infile是 JPEG,则lepton写入一个名为 的 Lepton 文件outfile;如果infile是 Lepton 文件,则lepton写入名为outfile. 使用这种双路径语法,lepton不会以任何方式更改您指定的输出文件名。它不会添加或删除扩展名或附加_s 来解决命名冲突。如果outfile已经存在,则将其覆盖。
您可能想要这样,但如果不是,并且您使用此语法,则必须通过让脚本调整输出文件名来自己解决问题。lepton当仅使用一个路径参数运行时,您可能能够以比自己的方案更好的方式来执行此操作。但我不会试图猜测您的具体需求和偏好;我将只使用单路径语法。
findtofile您展示的脚本试图通过在命令替换中运行file $(find ./ )来将每个参数传递给一个路径。这通常不起作用,因为在文件名可以包含的空格上拆分。文件 - 尤其是图像! - 和文件夹的名称中包含空格是很常见的。您显示的脚本将路径视为两条路径,并且. 在最好的情况下,两者都不存在;如果他们这样做,你会无意中操作错误的东西。并且根本不会处理原始路径。filefind$(find ./ )./abc/foo bar.jpg./abc/foobar.jpg
虽然可以通过设置IFS=$'\n'so 分词只在行之间进行(\n代表换行符)来减少这个问题的广度,但这不是一个好的解决方案。除了笨拙之外,它仍然可能失败,因为文件和目录名称可能包含换行符。我建议不要用它们命名文件或目录,除非测试程序或脚本的错误。但是可以创建这样的名称,包括在您不期望它们的意外情况下。文件名不能包含的唯一字符是路径分隔符/和空字符。因此,空字符是唯一一个不能出现在路径中的字符,也是分隔任意路径列表的唯一安全选择。这就是为什么find有-print0行动,xargs有-0选择。
这可以正确完成,find . -print0 | xargs -0 ...但您不需要第三个实用程序来传递从find到 的路径file。find的-exec动作就足够了。-exec构建要运行的命令之后的参数,直到\;或+。find ... -exec ... ;每个文件运行一次命令,同时find ... -exec ... +在每次运行时传递尽可能多的路径,这通常更快。通常所有参数都适合并且命令只运行一次。在极少数情况下,命令行会太长并且会find多次运行该命令。因此,该+表单仅对于运行以下命令是安全的:(a)在末尾使用它们的路径参数,并且(b)在具有多个文件名的一次运行中与在单独运行中的工作方式相同。
lepton是一个不能使用+形式运行的命令示例,-exec因为它不接受多个源文件名。第一个是输入,第二个是输出,其他的都是多余的。但是许多命令在使用多个参数运行一次时会做同样的事情,就像使用一个参数运行多次一样,并且file是其中之一。
此命令将生成表:
find . -exec file --mime-type -r0F '' {} +
Run Code Online (Sandbox Code Playgroud)
find在{}调用时用路径替换参数file,并+用尽可能多的附加路径参数替换。
--mime-type -r0F ''传递给的选项find解释如下。
有些人引用 {},例如,'{}'。这样做很好,但是 Bash 和其他 Bourne 风格的 shell 都不需要它。Bash 和其他一些 shell 支持大括号扩展,但不扩展一对空大括号。我选择不引用{},因为人们认为引用会{}阻止find进行分词。即使你的 shell 需要{}被引用,这仍然与分词无关,因为find从来没有这样做。(如果你想分词,你必须要告诉find给-exec一个shell)。而find如果你已经写了不能告诉{}或'{}'--the壳圈'{}'into {}(在引用删除期间),然后将其传递给find.
file我必须将一些选项传递给file-- 并且不能只使用 --的原因是默认情况下生成find . -exec file {} +的表file是不明确的:
01. Milan wide-angle sunset: JPEG image data, JFIF standard 1.01, resolution (DPI), density 1x1, segment length 16, baseline, precision 8, 1400x1400, frames 3
02. Kyle birthday party prep - blooper cakes: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 512x512, frames 3
first line
second line: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 500x500, frames 3
Run Code Online (Sandbox Code Playgroud)
那三排看起来像四排;一个文件名包含换行符。文件名也可以包含冒号,因此文件名的结束位置并不总是很清楚。比上面显示的更令人困惑的例子是可能的。
描述列还有比我们需要的更多的信息。Byte Commander 解释grep了JPEG在每一整行中返回错误结果的一个原因:名称中包含的非 JPEG 文件JPEG给出了误报。(检查类型的重点是你不能依赖名称,所以这是你展示的脚本中一个相当自毁的错误。)但即使你知道你正在查看描述列,它可能JPEG即使这不是类型,仍然包含:
$ touch empty.JPEG # not a JPEG
$ gzip -k empty.JPEG
$ file empty.JPEG*
empty.JPEG: empty
empty.JPEG.gz: gzip compressed data, was "empty.JPEG", last modified: Mon Aug 28 16:37:56 2017, from Unix
Run Code Online (Sandbox Code Playgroud)
字节指挥官的答案所解决此的(a)使所述-b选项file,导致其以省略的路径,:分离器和空格的类型,则前面(b)中使用grep,以检查是否描述开始与JPEG(所述^ 锚在图案^JPEG image data,做这个)。如果您跟踪传递给的路径,这将起作用——file对于 Byte Commander 的方法来说不是问题,file无论如何,它分别为每个路径运行。
我必须使用不同的解决方案,因为我的目标是从的输出中解析路径和类型,file以便file不必为每个文件单独运行。幸运的是file在 Ubuntu 中有很多选择。我使用:file --mime-type -r0F '' paths
--mime-type打印MIME 类型而不是详细说明。这就是我所需要的,然后我就可以对整个事物进行精确匹配。对于 JPEG,file --mime-type显示image/jpeg在说明栏中。(另见??s??? 的回答。)man file,-r导致无法打印的字符不被替换为八进制转义,如\003. 我相信我需要添加一个步骤来将这些序列转换回实际字符,这可能无法可靠地完成——如果这样的序列字面上出现在文件名中怎么办?(file 不转义\为\\。)我说“我相信”,因为我还没有设法file打印出这样的转义序列,而且我不确定它是否真的在文件名列中这样做了。不管怎样,-r这里是安全的。-0是这里的关键选项。没有它,这种方法就不能可靠地工作。它使file打印空字符 - 路径中永远不允许的一个字符,因为它通常用于在 C 程序中标记字符串的结尾 - 紧跟在文件名之后。这标志着表格两列之间的每一行中的中断。-F ''使file什么都不打印(''是一个空参数)而不是:. 冒号是不可靠的(它可以出现在文件名中)并且在这里没有任何好处,因为已经打印了一个空字符来指示路径列的结束和描述列的开始。为了find运行我使用. 的操作替换为路径。file --mime-type -r0F '' paths-exec file --mime-type -r0F '' {} +find-exec{} +
我以这种方式创建了表格:
find . -exec file --mime-type -r0F '' {} +
Run Code Online (Sandbox Code Playgroud)
如上所述,这会在每个路径之后放置一个空字符。如果描述也以空值结尾,那会很方便,但file不会这样做——描述总是以换行符结尾。所以我必须交替阅读直到一个空字符,然后假设有更多的文本并阅读它直到换行。我必须为每个文件执行此操作,并在没有任何内容时停止。
这种组合——读取可能包含换行符直到出现空字符的文本,然后读取在出现换行符之前不能包含换行符的文本——不是任何常见的 Unix 实用程序通常使用的方式。我将采用的方法是将 的输出通过管道find传输到循环中。循环的每次迭代都使用read内置的shell 两次读取表的一行,并使用不同的选项。
要读取路径,我使用:
read -rd ''
Run Code Online (Sandbox Code Playgroud)
-r是read唯一的标准选项,您几乎应该总是使用它。没有它,像\n从输入中一样的反斜杠转义被翻译成它们所代表的字符。我们不想那样。read读取直到看到换行符。为了忽略换行符并在空字符处停止,我使用-dBash 提供的选项来指定不同的字符。对于空字符,传递空参数''。-d选项),所以当没有变量名传递给 .bashrc 时,我也可以利用 Bash 的默认行为read。它将它读取的所有内容——除了终止字符——放在特殊变量中$REPLY。通常从输入的开头和结尾read去除空格($IFS字符),这是一种常见的习惯用法,IFS= read ...可以防止这种情况发生。$REPLY在 Bash 中隐式读取 to时,这不是必需的。要阅读说明,我使用:
read -r mimetype
Run Code Online (Sandbox Code Playgroud)
-r,read否则最好传递给它。 \mimetype。IFS=防止前导和尾随空格被剥离是很重要的。我想把它去掉。这会删除描述开头的空格,find以便在终端中显示表格时使表格更具可读性。只要有另一条路径要读取,循环就应该继续。该read命令在成功读取某些内容时返回 true(在 shell 编程中,这是零,与几乎所有其他编程语言不同),如果没有,则返回 false(在 shell 编程中,任何非零值)。所以常用的while read习语在这里很有用。我将 ( |) 的输出——这find是一个或(很少)更多file命令的输出——传送到while循环。
find . -exec file --mime-type -r0F '' {} + | while read -rd ''; do
read -r mimetype
# Commands using "$REPLY" and "$mimetype" go here.
done
Run Code Online (Sandbox Code Playgroud)
在循环内部,我阅读了该行的其余部分以获取描述 ( read -r mimetype)。我不费心检查这是否成功。即使遇到错误,也file应该只输出完整的行。(将错误和警告消息发送到标准错误,因此它们不会出现在管道中以破坏表。)您应该能够依赖这一点。file
如果你想检查是否read -r mimetype成功,你可以使用if. 或者您可以将其包含在while循环条件中:
find . -exec file --mime-type -r0F '' {} + |
while read -rd '' && read -r mimetype; do
# Commands using "$REPLY" and "$mimetype" go here.
done
Run Code Online (Sandbox Code Playgroud)
您可以看到我还拆分了第一行以提高可读性。(不需要\在 处拆分|。)
如果要在继续之前测试循环,可以将此命令放在(或代替)# Commands...注释下:
printf '[%s] [%s]\n\n' "$REPLY" "$mimetype"
Run Code Online (Sandbox Code Playgroud)
循环输出看起来像这样,具体取决于您在目录中的内容(为了简洁起见,我省略了大多数条目):
[.] [inode/directory]
[./stuv] [inode/x-empty]
[./ghi
jkl] [inode/x-empty]
[./fo.o/abc
def ] [image/jpeg]
[./fo.o/wyz.lep] [application/octet-stream]
[./fo.o/wyz] [image/jpeg]
Run Code Online (Sandbox Code Playgroud)
这只是为了查看循环是否正常工作。配售表的条目,[ ]这样无助于脚本做它需要做的,因为路径可能包括[,]和连续的换行。
在循环的每次迭代中,"$REPLY"包含路径并"$mimetype"包含类型描述。要确定是否"$REPLY"为 JPEG 文件命名,请检查是否"$mimetype"恰好是image/jpeg.
您可以使用if和[/ test(或[[)将字符串与=. 但我更喜欢case:
find -exec file --mime-type -r0F '' {} + | while read -rd ''; do
read -r mimetype
case "$mimetype" in image/jpeg)
# Put commands here that use "$REPLY".
;;
esac
done
Run Code Online (Sandbox Code Playgroud)
如果您只想以与上述相同的格式显示 JPEG 的路径——以帮助测试包含换行符的路径——整个case...esac语句可以是:
case "$mimetype" in image/jpeg) printf '[%s]\n\n' "$REPLY";; esac
Run Code Online (Sandbox Code Playgroud)
但目标是lepton在每个 JPEG 文件上运行。为此,请使用:
case "$mimetype" in image/jpeg) lepton "$REPLY";; esac
Run Code Online (Sandbox Code Playgroud)
让我们用 Bash 的特殊 globs 和一个for循环来做到这一点:
#!/bin/bash
shopt -s globstar dotglob
for f in ./** ; do
if file -b -- "$f" | grep -q '^JPEG image data,' ; then
# do whatever you want with the JPEG file "$f" in here:
md5sum -- "$f"
fi
done
Run Code Online (Sandbox Code Playgroud)
首先,我们需要通过启用globstar和dotglobshell 选项使 Bash glob 更有用。这是他们man bash在 SHELL BUILTIN COMMANDS 部分的描述shopt:
dotglob
If set, bash includes filenames beginning with a `.' in the results of
pathname expansion.
globstar
If set, the pattern ** used in a pathname expansion context will match
all files and zero or more directories and subdirectories. If the pattern
is followed by a /, only directories and subdirectories match.
Run Code Online (Sandbox Code Playgroud)
然后我们./**在for循环中使用这个新的“递归全局”来迭代当前目录及其所有子目录中的所有文件和文件夹。请始终使用以./或开头的绝对路径或显式相对路径../,而不仅仅是**,以防止出现特殊文件名(如~.
现在我们使用file命令测试每个文件(和文件夹)名称的内容。该-b选项防止它在内容信息字符串之前再次打印文件名,这使得过滤更加安全。
现在我们知道所有有效的 JPG/JPEG 文件的内容信息都必须以 开头JPEG image data,,这就是我们测试filefor with的输出grep。我们使用该-q选项来抑制任何输出,因为我们只对grep的退出代码感兴趣,它指示模式是否匹配。
如果匹配,将执行if/then块中的代码。我们可以在这里做任何我们想做的事。当前 JPEG 文件名在 shell 变量中可用$f。我们只需要确保始终将其放在双引号中,以防止意外评估带有特殊字符(如空格、换行符或符号)的文件名。这也是通常最好通过后,将其从其他参数分开--,这导致大多数命令将其解释为,即使它的类似的文件名-v或--help否则将被解释为一个选项。
为了科学,是时候炸毁一些代码了!这是您的问题/书中的版本:
for jpeg in `echo "$(file $(find ./ )
| grep JPEG | cut -f 1 -d ':')"`
do
/path/to/command "$jpeg"
done
Run Code Online (Sandbox Code Playgroud)
首先,请允许我提一下他们写的有多复杂。我们有 4 级嵌套子 shell,使用混合命令替换语法(``和$()),这是因为find.
这里find只列出所有文件并打印它们的名称,每行一个。然后将完整的输出传递file给检查它们中的每一个。可是等等!每行一个文件名?包含换行符的文件名怎么样?对,那些会打破它!
$ ls --escape ne*ne
new\nline
$ file $(find . -name 'ne*ne' )
./new: cannot open `./new' (No such file or directory)
line: cannot open `line' (No such file or directory)
Run Code Online (Sandbox Code Playgroud)
实际上,即使是简单的空格也会破坏它,因为它们也被file. 您甚至不能引用"$(find ./ )"此处作为补救措施,因为那样会将整个多行输出引用为一个文件名参数。
$ ls simple*
simple spaces.jpg
$ file $(find ./ -name 'simple*')
./simple: cannot open `./simple' (No such file or directory)
spaces.jpg: cannot open `spaces.jpg' (No such file or directory)
Run Code Online (Sandbox Code Playgroud)
下一步,file输出被扫描grep JPEG。难道你不认为欺骗这样一个简单的模式有点容易,尤其是在plain 的输出file总是包含文件名的情况下?基本上,文件名中带有“JPEG”的所有内容都会触发匹配,无论它包含什么内容。
$ echo "to be or not to be" > IAmNoJPEG.txt
$ file IAmNoJPEG.txt | grep JPEG
IAmNoJPEG.txt: ASCII text
Run Code Online (Sandbox Code Playgroud)
好的,所以我们有file所有 JPEG 文件的输出(或那些假装的文件),现在他们处理所有行以cut从第一列中提取原始文件名,用冒号分隔......猜猜看,让我们试试这在名称中带有冒号的文件上:
$ ls colon*
colons:evil.jpeg
$ file colon* | grep JPEG | cut -f 1 -d ':'
colons
Run Code Online (Sandbox Code Playgroud)
所以总而言之,您书中的方法有效,但前提是它检查的所有文件不包含任何空格、换行符、冒号和可能的其他特殊字符,并且文件名中的任何位置都不包含字符串“JPEG”。它也有点丑陋,但由于美丽在于旁观者的眼中,我不会漫无边际地谈论它。
| 归档时间: |
|
| 查看次数: |
1251 次 |
| 最近记录: |