使用 Bash 脚本创建动态壁纸

利用 bash 的强大功能,学习如何每天早上从网站抓取令人兴奋的新图片。

所以,你想要一个炫酷的动态桌面壁纸,而又不想使用可疑的程序和一堆病毒?好消息是,这里是 Linux,一切皆有可能。我开始这个项目是因为我对标准的操作系统桌面壁纸感到厌烦,并且我慢慢地创建了大量的脚本,从几个网站拉取图片并将它们设置为我的桌面背景。这为我的一天增添了一点乐趣——每天早上迎接我的是不同的猫咪照片或是我以前不知道存在的国家的全景图。好消息是这很容易做到,所以让我们开始吧。

为什么选择 Bash?

Bash (Bourne Again shell) 是几乎所有 *NIX 系统的标准配置,并提供了广泛的“开箱即用”操作,这些操作如果使用传统的编程甚至脚本语言来实现,将需要时间和大量的代码。此外,没有必要重新发明轮子。使用别人的程序来下载网页,例如,比在 C 语言中处理底层系统套接字要容易得多。

它将如何工作?

概念很简单。选择一个你喜欢的图片的网站,并“抓取”页面以获取这些图片。然后,一旦你有了直接链接,你就可以下载它们并使用显示管理器将它们设置为桌面壁纸。很简单,对吧?

一个简单的例子:xkcd

首先,让我们访问每个程序员继 Stack Overflow 之后第二喜欢的页面:xkcd。加载页面后,您应该会看到每日漫画和一些其他数据。

现在,如果您想在不访问 xkcd 网站的情况下查看此漫画怎么办?您需要一个脚本来为您完成它。首先,您需要了解网页在计算机上的外观,因此请下载它并查看一下。为此,请使用 wget,这是一个易于使用、常用安装、非交互式的网络下载器。因此,在命令行上,调用 wget,并为其提供页面链接


user@LJ $: wget https://www.xkcd.com/


--2018-01-27 21:01:39--  https://www.xkcd.com/
Resolving www.xkcd.com... 151.101.0.67, 151.101.192.67,
 ↪151.101.64.67, ...
Connecting to www.xkcd.com|151.101.0.67|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2606 (2.5K) [text/html]
Saving to: 'index.html'

index.html                                  100%
[==========================================================>]
2.54K  --.-KB/s    in 0s

2018-01-27 21:01:39 (23.1 MB/s) - 'index.html' saved [6237]

正如您在输出中看到的,该页面已保存到您当前目录中的 index.html。使用您最喜欢的编辑器打开它并查看一下(在此示例中我使用 nano)


user@LJ $: nano index.html

现在您可能会意识到,尽管这是一个相当简洁的页面,但该文件中有很多代码。与其全部浏览一遍,不如使用 grep,它非常适合这项任务。它的唯一功能是打印与您的搜索匹配的行。Grep 使用以下语法


user@LJ $: grep [search] [file]

查看每日漫画,其当前标题为“Night Sky”。使用 grep 搜索“night”会产生以下结果


user@LJ $: grep "night" index.html

<img src="//imgs.xkcd.com/comics/night_sky.png"
 ↪title="There's a mountain lion nearby, but it
 ↪didn't notice you because it's
 ↪reading Facebook." alt="Night Sky"
 ↪srcset="//imgs.xkcd.com/comics/night_sky_2x.png 2x"/>
Image URL (for hotlinking/embedding):
 ↪https://imgs.xkcd.com/comics/night_sky.png

grep 搜索在文件中返回了两个图像链接,每个链接都与“night”相关。查看这两行,其中一个是页面中的图像,另一个用于热链接,并且已经是可用的链接。但是,您将获得第一个链接,因为它更能代表其他不提供简单链接的页面,并且可以很好地介绍 grepcut 的用法。

要从页面中获取第一个链接,您首先需要在文件中以编程方式识别它。让我们再次尝试 grep,但这次不是使用您已经知道的字符串(“night”),而是假设您对该页面一无所知。尽管链接会有所不同,但 HTML 应该保持不变;因此,<img src= 应该始终出现在您想要的链接之前


user@LJ $: grep "img src=" index.html

<span><a href="/"><img src="/s/0b7742.png" alt="xkcd.com logo"
 ↪height="83" width="185"/></a></span>
<img src="//imgs.xkcd.com/comics/night_sky.png"
 ↪title="There's a mountain lion nearby, but it
 ↪didn't notice you because it's reading Facebook."
 ↪alt="Night Sky" srcset="//imgs.xkcd.com/comics/
↪night_sky_2x.png 2x"/>
<img src="//imgs.xkcd.com/s/a899e84.jpg" width="520"
 ↪height="100" alt="Selected Comics" usemap="#comicmap"/>

看起来页面上有三张图片。比较第一次 grep 的这些结果,您会看到 <img src="//imgs.xkcd.com/comics/night_sky.png" 再次返回。这是您想要的图像,但是如何将其与其他两个图像分开呢?最简单的方法是通过另一个 grep 传递它。其他两个链接包含“/s/”;而我们想要的链接包含“/comics/”。因此,您需要 grep 上一个命令的输出,以查找“/comics/”。要传递上一个命令的输出,请使用管道字符 (|)


user@LJ $: grep "img src=" index.html | grep "/comics/"

<img src="//imgs.xkcd.com/comics/night_sky.png"
 ↪title="There's a mountain lion nearby, but it
 ↪didn't notice you because it's reading Facebook."
 ↪alt="Night Sky" srcset="//imgs.xkcd.com/comics/
↪night_sky_2x.png 2x"/>

好了,这就是那一行!现在您只需要使用 cut 命令将图像链接与其余部分分开。cut 使用以下语法


user@LJ $: cut [-d  delimeter] [-f field] [-c characters]

要从行的其余部分剪切链接,您需要剪切引号旁边,并选择下一个引号之前的字段。换句话说,您想要引号之间的文本,即链接,就像这样完成


user@LJ $: grep "img src=" index.html | grep "/comics/" |
 ↪cut -d\" -f2

//imgs.xkcd.com/comics/night_sky.png

好了,您已经获得了链接。但是等等!开头的那些讨厌的正斜杠呢?您也可以将它们剪切掉


user@LJ $: grep "img src=" index.html | grep "/comics/" |
 ↪cut -d\" -f 2 | cut -c 3-

imgs.xkcd.com/comics/night_sky.png

现在您刚刚从该行中剪切了前三个字符,剩下的就是一个直接指向图像的链接。再次使用 wget,您可以下载该图像


user@LJ $: wget imgs.xkcd.com/comics/night_sky.png


--2018-01-27 21:42:33--  http://imgs.xkcd.com/comics/night_sky.png
Resolving imgs.xkcd.com... 151.101.16.67, 2a04:4e42:4::67
Connecting to imgs.xkcd.com|151.101.16.67|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 54636 (53K) [image/png]
Saving to: 'night_sky.png'

night_sky.png                               100%
[===========================================================>]
53.36K  --.-KB/s    in 0.04s

2018-01-27 21:42:33 (1.24 MB/s) - 'night_sky.png'
 ↪saved [54636/54636]

现在您的目录中有了图像,但是当漫画的名称更改时,它的名称也会更改。要解决此问题,请告诉 wget 使用特定名称保存它


user@LJ $: wget "$(grep "img src=" index.html | grep "/comics/"
 ↪| cut -d\" -f2 | cut -c 3-)" -O wallpaper
--2018-01-27 21:45:08--  http://imgs.xkcd.com/comics/night_sky.png
Resolving imgs.xkcd.com... 151.101.16.67, 2a04:4e42:4::67
Connecting to imgs.xkcd.com|151.101.16.67|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 54636 (53K) [image/png]
Saving to: 'wallpaper'

wallpaper                                   100%
[==========================================================>]
53.36K  --.-KB/s    in 0.04s

2018-01-27 21:45:08 (1.41 MB/s) - 'wallpaper' saved [54636/54636]

-O 选项意味着下载的图像现在已保存为“wallpaper”。现在您知道了图像的名称,您可以将其设置为壁纸。这取决于您使用的显示管理器。下面列出了最流行的显示管理器,假设图像位于 /home/user/wallpaper。

GNOME


gsettings set org.gnome.desktop.background picture-uri
 ↪"File:///home/user/wallpaper"
gsettings set org.gnome.desktop.background picture-options
 ↪scaled

Cinnamon


gsettings set org.cinnamon.desktop.background picture-uri
 ↪"file:///home/user/wallpaper"
gsettings set org.cinnamon.desktop.background picture-options
 ↪scaled

Xfce


xfconf-query --channel xfce4-desktop --property
 ↪/backdrop/screen0/monitor0/image-path --set
 ↪/home/user/wallpaper

您现在可以设置壁纸了,但是您需要不同的图像来混合使用。查看网页,有一个“random”按钮,可将您带到随机漫画。使用 grep 搜索“random”会返回以下内容


user@LJ $: grep random index.html

<li><a href="//c.xkcd.com/random/comic/">Random</a></li>
<li><a href="//c.xkcd.com/random/comic/">Random</a></li>

这是指向随机漫画的链接,使用 wget 下载并读取结果,看起来像是初始漫画页面。成功!

现在您已经拥有了所有组件,让我们将它们组合成一个脚本,将 www.xkcd.com 替换为新的 c.xkcd.com/random/comic/


#!/bin/bash

wget c.xkcd.com/random/comic/

wget "$(grep "img src=" index.html | grep /comics/ | cut -d\"
 ↪-f 2 | cut -c 3-)" -O wallpaper

gsettings set org.gnome.desktop.background picture-uri
 ↪"File:///home/user/wallpaper"
gsettings set org.gnome.desktop.background picture-options
 ↪scaled

除了第一行(将其指定为 bash 脚本)和第二个 wget 命令外,所有这些都应该是熟悉的。要将命令的输出捕获到变量中,您可以使用 $()。在这种情况下,您正在捕获 grepping 和 cutting 过程——捕获最终链接,然后使用 wget 下载它。当脚本运行时,括号内的命令都会运行,生成图像链接,然后在调用 wget 下载它之前。

就这样,您就得到了一个简单的动态壁纸示例,您可以随时运行它。

如果您希望脚本自动运行,您可以添加一个 cron 作业,让 cron 为您运行它。因此,请使用以下命令编辑您的 cron 表


user@LJ $: crontab -e

我的脚本名为“xkcd”,我的 crontab 条目如下所示


@reboot /bin/bash /home/user/xkcd

这将使用 bash 在每次重启时运行脚本(位于 /home/user/xkcd)。

Reddit

上面的脚本展示了如何在 HTML 代码中搜索图像并下载它们。但是,您可以将其应用于您选择的任何网站——尽管 HTML 代码会有所不同,但基本概念保持不变。考虑到这一点,让我们来解决从 Reddit 下载图像的问题。为什么选择 Reddit?Reddit 可能是互联网上最大的博客,也是美国第三大最受欢迎的网站。它将来自许多不同社区的内容聚合到一个网站上。它通过使用“subreddit”(子版块)来实现这一点,这些社区联合起来形成 Reddit。为了本文的目的,让我们重点关注主要处理图像的 subreddit(或简称“subs”)。但是,任何 subreddit,只要它允许图像,都可以在此脚本中使用。

Screenshot

图 1. 简化网页抓取—在终端中分析网页

深入探讨

就像 xkcd 脚本一样,您需要从 subreddit 下载网页以对其进行分析。在此示例中,我使用 reddit.com/r/wallpapers。首先,检查 HTML 中的图像


user@LJ $: wget https://www.reddit.com/r/wallpapers/ && grep
 ↪"img src=" index.html

--2018-01-28 20:13:39--  https://www.reddit.com/r/wallpapers/
Resolving www.reddit.com... 151.101.17.140
Connecting to www.reddit.com|151.101.17.140|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 27324 (27K) [text/html]
Saving to: 'index.html'

index.html                                  100%
[==========================================================>]
26.68K  --.-KB/s    in 0.1s

2018-01-28 20:13:40 (270 KB/s) - 'index.html' saved [169355]

</div></form><div class="bottom"><span class="age">a community
 ↪for <time title="Thu May 1 16:17:13 2008 UTC"
 ↪datetime="2008-05-01T16:17:13+00:00">9 years</time></span>
 ↪....Forever and ever......

--- SNIP ---

所有图像都以一行长线返回,因为图像的 HTML 也在一行长线中。您需要将这一行长线拆分为单独的图像链接。输入正则表达式。

Regex 是 regular expression(正则表达式)的缩写,正则表达式是许多程序使用的一种系统,允许用户将表达式与字符串匹配。它包含通配符,这些通配符是匹配特定字符的特殊字符。例如,* 字符将匹配每个字符。对于此示例,您需要一个与 HTML 文件中的每个链接匹配的表达式。所有 HTML 链接都有一个共同的字符串。它们都采用 href="LINK" 的形式。让我们编写一个正则表达式来匹配


href="([^"#]+)"

现在让我们分解一下

  • href=" — 简单地说明前几个字符应与这些字符匹配。

  • () — 形成捕获组。

  • [^] — 形成否定集。字符串不应匹配内部的任何字符。

  • + — 字符串应匹配一个或多个前面的标记。

总而言之,正则表达式匹配一个以 href= 开头的字符串,不包含任何引号或井号标签,并以引号结尾。

此正则表达式可以与 grep 一起使用,如下所示


user@LJ $: grep -o -E 'href="([^"#]+)"' index.html

href="/static/opensearch.xml"
href="https://www.reddit.com/r/wallpapers/"
href="//out.reddit.com"
href="//out.reddit.com"
href="//www.redditstatic.com/desktop2x/img/favicon/
↪apple-icon-57x57.png"

--- SNIP ---

-e 选项允许使用扩展正则表达式选项,而 -o 开关意味着 grep 将仅打印完全匹配的模式,而不是整行。现在,您有了一个更易于管理的链接列表。从那里,您可以使用第一个脚本中的相同技术来提取链接并过滤图像。它看起来像这样


user@LJ $: grep -o -E 'href="([^"#]+)"' index.html | cut -d'"'
 ↪-f2 | sort | uniq | grep -E '.jpg|.png'

https://i.imgur.com/6DO2uqT.png
https://i.imgur.com/Ualn765.png
https://i.imgur.com/UO5ck0M.jpg
https://i.redd.it/s8ngtz6xtnc01.jpg
//www.redditstatic.com/desktop2x/img/favicon/
↪android-icon-192x192.png
//www.redditstatic.com/desktop2x/img/favicon/
↪apple-icon-114x114.png
//www.redditstatic.com/desktop2x/img/favicon/
↪apple-icon-120x120.png
//www.redditstatic.com/desktop2x/img/favicon/
↪apple-icon-144x144.png
//www.redditstatic.com/desktop2x/img/favicon/
↪apple-icon-152x152.png
//www.redditstatic.com/desktop2x/img/favicon/
↪apple-icon-180x180.png
//www.redditstatic.com/desktop2x/img/favicon/
↪apple-icon-57x57.png
//www.redditstatic.com/desktop2x/img/favicon/
↪apple-icon-60x60.png
//www.redditstatic.com/desktop2x/img/favicon/
↪apple-icon-72x72.png
//www.redditstatic.com/desktop2x/img/favicon/
↪apple-icon-76x76.png
//www.redditstatic.com/desktop2x/img/favicon/
↪favicon-16x16.png
//www.redditstatic.com/desktop2x/img/favicon/
↪favicon-32x32.png
//www.redditstatic.com/desktop2x/img/favicon/
↪favicon-96x96.png

最后的 grep 再次使用正则表达式来匹配 .jpg 或 .png。| 字符充当布尔 OR 运算符。

如您所见,实际图像有四个匹配项:两个 .jpg 和两个 .png。其他的是 Reddit 默认图像,例如徽标。删除这些图像后,您将获得最终的图像列表以设置为壁纸。从列表中删除这些图像的最简单方法是使用 sed


user@LJ $: grep -o -E 'href="([^"#]+)"' index.html | cut -d'"'
 ↪-f2 | sort | uniq | grep -E '.jpg|.png' | sed /redditstatic/d

https://i.imgur.com/6DO2uqT.png
https://i.imgur.com/Ualn765.png
https://i.imgur.com/UO5ck0M.jpg
https://i.redd.it/s8ngtz6xtnc01.jpg

sed 的工作原理是匹配两个正斜杠之间的内容。末尾的 d 告诉 sed 删除与模式匹配的行,留下图像链接。

从 Reddit 获取图像的好处是,每个 subreddit 都包含几乎相同的 HTML;因此,这个小脚本将适用于任何 subreddit。

创建脚本

要为 Reddit 创建脚本,应该可以从您想要从中获取图像的 subreddit 中进行选择。我为我的脚本创建了一个目录,并在目录中放置了一个名为“links”的文件。此文件包含以下格式的 subreddit 链接


https://www.reddit.com/r/wallpapers
https://www.reddit.com/r/wallpaper
https://www.reddit.com/r/NationalPark
https://www.reddit.com/r/tiltshift
https://www.reddit.com/r/pic

在运行时,我让脚本读取列表并下载这些 subreddit,然后再从中剥离图像。

由于您一次只能将一张图像作为桌面壁纸,因此您需要将图像的选择范围缩小到一张。但是,首先,最好拥有广泛的图像范围,而又不使用大量带宽。因此,您需要下载多个 subreddit 的网页并剥离图像链接,但不下载图像本身。然后,您将使用随机选择器选择一个图像链接并下载该链接以用作壁纸。

最后,如果您要下载大量 subreddit 的网页,脚本将变得非常慢。这是因为脚本在继续之前会等待每个命令完成。为了避免这种情况,您可以通过附加一个 & 符号来 fork 一个命令。这将为命令创建一个新进程,将其从主进程(脚本)“fork”出来。

这是我的完整注释脚本


#!/bin/bash

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 ↪# Get the script's current directory

linksFile="links"

mkdir $DIR/downloads
cd $DIR/downloads

# Strip the image links from the html
function parse {
grep -o -E 'href="([^"#]+)"' $1 | cut -d'"' -f2 | sort | uniq
 ↪| grep -E '.jpg|.png' >> temp
grep -o -E 'href="([^"#]+)"' $2 | cut -d'"' -f2 | sort | uniq
 ↪| grep -E '.jpg|.png' >> temp
grep -o -E 'href="([^"#]+)"' $3 | cut -d'"' -f2 | sort | uniq
 ↪| grep -E '.jpg|.png' >> temp
grep -o -E 'href="([^"#]+)"' $4 | cut -d'"' -f2 | sort | uniq
 ↪| grep -E '.jpg|.png' >> temp
}

# Download the subreddit's webpages
function download {
rname=$( echo $1 | cut -d / -f 5  )
tname=$(echo t.$rname)
rrname=$(echo r.$rname)
cname=$(echo c.$rname)
wget --load-cookies=../cookies.txt -O $rname $1
 ↪&>/dev/null &
wget --load-cookies=../cookies.txt -O $tname $1/top
 ↪&>/dev/null &
wget --load-cookies=../cookies.txt -O $rrname $1/rising
 ↪&>/dev/null &
wget --load-cookies=../cookies.txt -O $cname $1/controversial
 ↪&>/dev/null &
wait # wait for all forked wget processes to return
parse $rname $tname $rrname $cname
}


# For each line in links file
while read l; do
   if [[ $l != *"#"* ]]; then # if line doesn't contain a
 ↪hashtag (comment)
        download $l&
   fi
done < ../$linksFile

wait # wait for all forked processes to return

sed -i '/www.redditstatic.com/d' temp # remove reddit pics that
 ↪exist on most pages from the list


wallpaper=$(shuf -n 1 temp) # select randomly from file and DL

echo $wallpaper >> $DIR/log # save image into log in case
 ↪we want it later

wget -b $wallpaper -O $DIR/wallpaperpic 1>/dev/null # Download
 ↪wallpaper image

gsettings set org.gnome.desktop.background picture-uri
 ↪file://$DIR/wallpaperpic # Set wallpaper (Gnome only!)


rm -r $DIR/downloads # cleanup

就像之前一样,您可以设置一个 cron 作业,以便在每次重启或您喜欢的任何时间间隔运行脚本。

就这样,您就得到了一个功能齐全的猫咪图片采集器。愿您的早晨登录时迎接您的是许多毛茸茸的脸庞。现在开始探索新的 subreddit 以供欣赏,并探索新的网站以抓取炫酷的壁纸。

Patrick Whelan 是英国 Edge Hill 大学的一年级学生。他是一位有抱负的开发人员、博主和全能黑客。

加载 Disqus 评论