当前位置: 首页 > 科技观察

使用Shell搭建多进程CommandlineFu爬虫

时间:2023-03-20 11:38:38 科技观察

CommandlineFu是一个记录脚本片段的网站,每个片段都有对应的功能描述和对应的标签。我想做的就是尝试在shell中编写一个多进程爬虫,将这些代码片段记录在一个org文件中。参数定义本脚本需要能够通过-n参数指定爬虫并发数(默认为CPU核数),也能够通过-f指定保存的org文件路径(默认输出到标准输出)。#!/usr/bin/envbashproc_num=$(nproc)store_file=/dev/stdoutwhilegetopts:n:f:OPT;在n|+n中执行案例$OPT)proc_num="$OPTARG";;f|+f)store_file="$OPTARG";;*)echo"usage:${0##*/}[+-nproc_num][+-forg_file}[--]"exit2esacdoneshift$((OPTIND-1))OPTIND=1解析命令浏览页面我们需要一个进程从CommandlineFu的浏览列表中提取每个脚本片段的URL,这个过程会将提取的URL存储在一个队列中,然后每个爬虫进程会从该进程中读取URL并提取相应的代码片段、描述和标签信息写入到org文件中。这里会遇到三个问题:如何实现进程间的通信队列如何从页面中提取URL、代码片段、描述、标签等信息多进程读写同一个时的乱序问题file更容易解决它们之间的通信队列问题。我们可以通过命名管道来实现:queue=$(mktemp--dry-run)mkfifo${queue}exec99<>${queue}trap"rm${queue}2>/dev/null"EXIT提取从页面中提取所需信息从页面中提取元素内容主要有两种方法:对于简单的HTML页面,我们可以使用诸如sed、grep、awk等工具匹配正则表达式的方式从HTML中提取信息。使用html-xml-utils工具集中的hxselect根据CSS选择器提取相关元素。这里我们使用html-xml-utils工具进行提取:functionextract_views_from_browse_page(){if[[$#-eq0]];然后localhtml=$(cat-)elselocalhtml="$*"fiecho${html}|hxclean|hxselect-c-s"\n""li.list-group-item>div:nth-child(1)>div:nth-child(1)>a:nth-child(1)::attr(href)"|sed's@^@https://www.commandlinefu.com/@'}functionextract_nextpage_from_browse_page(){if[[$#-eq0]];thenlocalhtml=$(cat-)elselocalhtml="$*"fiecho${html}|hxclean|hxselect-s"\n""li.list-group-item:nth-child(26)>a"|grep'>'|hxselect-c"::attr(href)"|sed's@^@https://www.commandlinefu.com/@'}这里需要注意的是:hxselect在解析HTML时需要遵循严格的XML规范,所以在使用hxselect时需要先通过hxclean进行修正再进行解析。另外,为了防止HTML过大,超过参数列表的长度,允许通过管道传入HTML内容。循环读取下一页的浏览页面,不断提取代码片段url写入队列。上面提到的第三个问题,这里要解决的是:多进程读写管道时,如何保证不乱序?为此,我们需要在写文件的时候给文件加锁,写完文件再解锁。在shell中,我们可以使用flock来锁定文件。flock的使用和注意事项可以参考另一篇博文Linuxshellflock文件锁的使用和注意事项。由于flock子进程中需要用到函数extract_views_from_browse_page,所以需要先导出:export-fextract_views_from_browse_page由于网络问题,使用curl获取内容可能会失败,需要反复获取:functionfetch(){本地url="$1"而!curl-L${url}2>/dev/null;do:done}collector用于从seedurl中抓取要抓取的url写入pipeline文件。在写操作过程中,管道文件也被用作锁文件:functioncollector(){url="$*"while[[-n${url}]];doecho"Extractfrom$url"html=$(fetch"${url}")echo"${html}"|flock${queue}-c"extract_views_from_browse_page>${queue}"url=$(echo"${html}"|extract_nextpage_from_browse_page)done#让解析代码段的爬虫进程正常退出,不被阻塞。for((i=0;i<${proc_num};i++))doecho>${queue}done}这里需要注意的是,找不到下一页的url后,我们用for循环写到队列中输入=proc_num=空行。这一步的目的是让解析代码段的爬虫进程正常退出,不被阻塞。解析脚本片段页面,我们需要从脚本片段页面中提取出标题、代码片段、描述、标签信息,并将这些内容以org模式的格式写入到存储文件中。functionview_page_handler(){localurl="$1"localhtml="$(fetch"${url}")"#headlinelocalheadline="$(echo${html}|hxclean|hxselect-c-s"\n"".col-md-8>h1:nth-child(1)")"#commandlocalcommand="$(echo${html}|hxclean|hxselect-c-s"\n"".col-md-8>div:nth-child(2)>span:nth-child(2)"|pandoc-fhtml-torg)"#descriptionlocaldescription="$(echo${html}|hxclean|hxselect-c-s"\n"".col-md-8>div.description"|pandoc-fhtml-torg)"#tagslocaltags="$(echo${html}|hxclean|hxselect-c-s":"".functions>a")"if[[-n"${tags}"]];thentags=":${tags}"fi#构建组织内容cat<${store_file}*${headline}\t\t${tags}${description}#+begin_srcshell${command}#+end_srcEOF意思是用flock来锁定cat命令,然后将整个flock命令的结果直接输出到存储文件,而重定向输出的进程未被锁定。蜘蛛从管道文件中读取要爬取的URL,然后执行实际的爬取动作。functionspider(){while:doif!url=$(flock${queue}-c'read-t1-u99url&&echo$url')thensleep1continuefiif[[-z"$url"]];thenbreakfiview_page_handler${url}done}这里需要注意的是,为了防止死锁,在从管道读取URL的时候设置了一个超时时间。当发生超时时,意味着生产进程跟不上消费进程的消费速度,于是消费进程休眠一秒,然后再次检查队列中的URL。通过重新定义extract_views_from_browse_page,extract_nextpage_from-browse_page,view_page_handler联合采集器"https://www.commandlinefu.com/commands/browse"&for((i=0;i<${proc_num};i++))dospider&donewait爬取其他网站这些功能,并提供一个新的种子URL,我们可以很容易地将其转化为一个多进程爬虫来爬取其他网站。例如,下面的代码可以用来爬取xkcd上的漫画:functionextract_views_from_browse_page(){if[[$#-eq0]];thenlocalhtml=$(cat-)elselocalhtml="$*"fimax=$(echo"${html}"|hxclean|hxselect-c-s"\n""#middleContainer"|grep"永久链接到这部漫画"|awk-F"/"'{print$4}')seq1${max}|sed's@^@https://xkcd.com/@'}functionextract_nextpage_from_browse_page(){echo""}functionview_page_handler(){localurl="$1"localhtml="$(fetch"${url}/")"localimage="https:$(echo${html}|hxclean|hxselect-c-s"\n""#comic>img:nth-child(1)::attr(src)")"echo${image}wget${image}}collector"https://xkcd.com/"&