如何在
深圳图书馆
跟踪一本书
08
本文成于去年十二月,因写得太差不好意思发。现在二十多年过去了,活鱼格勒发了不少写得比这篇还要差得多的文,所以不如把这篇也发了吧。
既然我在图书馆失去了尤达大师,根据一些定理,易证我可以在图书馆把大师找回来。不仅要找回来,而且还要通知我,大师有没有被别人借走。
本文将以C#在.NET框架下实现针对深圳图书馆的爬虫程序,并加入提醒机制。像上一篇推送一样,在开始实现前我们需要讨论一些有关的问题。
![293a15f1e06592a4617f24c1db9aa710.gif](https://i-blog.csdnimg.cn/blog_migrate/ce93b73f85f214a6ab0aa16e0dfcbb02.gif)
HTTP请求
请求是一个很直观的过程:客户向服务器发送一个请求,服务器送回它的响应。发送请求的单位是报文,不同的网络协议要求不同格式的报文,这里不展开讨论。下面是我们将使用的HTTP协议的报文格式:
![32f6527b67e9f222d68f6a2b5b600f9b.png](https://i-blog.csdnimg.cn/blog_migrate/8f4047f72645ad330e1114e94cfa4775.png)
URL:接收请求的地址。一般来说浏览器上部地址框中展示的就是指向当前网页的URL。
请求方法:HTTP有六种请求方法,常用的是GET和POST。GET方法将请求的内容添加在URL后,以‘?’字符与URL主体分隔开来,因此使用GET方法的HTTP报文往往没有上图的“请求数据”。POST方法则将请求内容存放在报文末端的请求数据中。
由于GET格式的请求数据是直接包含在URL内的,而URL又必须按照相关协议规定以明文展示,所以可以很方便地分析GET请求。比如,百度搜索“晒布路”,实际请求的URL为:
![40c8f9da3aac98c9be72fe60ddf35e8b.png](https://i-blog.csdnimg.cn/blog_migrate/78f449f3a157e08ed25f10fb659835d9.png)
其中的……wd=%E6%99%92%E5%B8%83%E8%B7%AF是按照UTF-8编码转码“晒布路”三个中文字符得到的。这里不展开讨论字符编码和百分号编码格式,大部分浏览器甚至库函数会自动完成这种编码。
![293a15f1e06592a4617f24c1db9aa710.gif](https://i-blog.csdnimg.cn/blog_migrate/ce93b73f85f214a6ab0aa16e0dfcbb02.gif)
用C#实现HTTP请求
众所周知,.NET框架下有大量封装良好的库函数,相比于C/C++的关注底层实现,C#的关注于上层功能使人感到极度的愉悦。
为了实现GET方式的HTTP请求,我们使用HttpWebRequest与HttpWebResponse类,这两个类位于System.Net命名空间下。
---HttpWebRequest---
这个类支持大量的HTTP报文属性。在本例中我们只需要简单地提交一个GET请求,因此这里不展开讨论这些属性。
创建一个HttpWebRequest类:
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(webpath);
构造函数的参数即为请求的URL。
之后,设置这个实例的请求类型为GET:
request.Headers.Add("Accept-Encoding", "gzip, deflate");
---HttpWebResponse---
调用以上实例的GetResponse(),即获取响应函数,返回的是一个HttpWebResponse类,此类型保存响应的具体信息。在本例中,服务器响应的应是.html格式的网页文件,由于我们想要的信息“货真价实”地显示在了网页上,所以这些信息一定可以在这些网页文件中直接获取。
获取请求的响应:
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
为了获取完整的响应数据,这里新建一个数据流。流的概念不在本文讨论的范围内,可以简单地把这一操作理解为使储存在响应实例中的数据“流”向目标。
为了愉快地面对对象编程,我们把上面几个操作封装到一个新类型中:
namespace PieskiLib{ class WebRequest { internal string webpath; internal WebRequest(string webpath) { this.webpath = webpath; } internal string Download() { HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(webpath); request.Timeout = 36000; request.Method = "GET"; request.UserAgent = "Mozilla/4.0"; request.Headers.Add("Accept-Encoding", "gzip, deflate"); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream dld_stream = response.GetResponseStream(); StreamReader reader = new StreamReader(dld_stream, Encoding.UTF8); string html = reader.ReadToEnd(); dld_stream.Close(); dld_stream.Dispose(); reader.Close(); reader.Dispose(); return html; } }}
这个类型的成员函数Download()返回由构造函数确定的请求得到的全部响应数据字符串。在本例中,这个字符串包含的是html文件的全部代码文本。
![293a15f1e06592a4617f24c1db9aa710.gif](https://i-blog.csdnimg.cn/blog_migrate/ce93b73f85f214a6ab0aa16e0dfcbb02.gif)
获取图书馆数据
深图的书,多。我们找的是尤达大师,因此我们进入图书检索主页,在“书名”后输入“尤达大师”。
![00ffebf945462e4191ad62cafc314274.png](https://i-blog.csdnimg.cn/blog_migrate/bada50323730568399ac1ba90d5089af.png)
(https://www.szlib.org.cn/Search/search.jsp)
得到的结果只有一本,如下:
![db0e544f54b11f517f8d3fed4f7c3211.png](https://i-blog.csdnimg.cn/blog_migrate/80ef7351c8d730e18b531ef9914fc9a6.png)
这个标题让我产生了一些疑虑。
![5688d9721f29f12e311c0acf5d38a15a.png](https://i-blog.csdnimg.cn/blog_migrate/44f9c517ed86209c0114e442b4168146.jpeg)
![baa22068bafd1508ced96305c5709d6c.png](https://i-blog.csdnimg.cn/blog_migrate/fcfc0a7da7c0b0465c9503bb7a84a421.png)
![12d76caa333eadf5dd660cfe342d5224.png](https://i-blog.csdnimg.cn/blog_migrate/6782026ad38d8d288e94a6f0725bdb36.jpeg)
这和我想的大师有一定差距。不过这个例子还是可以展现深圳图书馆的图书检索框架:在首页输入书名/作者/出版社等书籍特征,发送请求,得到一个符合特征的图书列表。这个列表里的每一项包含了该书的详细信息及馆藏资源。
现在,摆在我们和自动获取数据之间的只有一个问题:这个系统的请求方式是GET吗?
![853159a0267a37add0e7e5d18d919ff0.png](https://i-blog.csdnimg.cn/blog_migrate/4d01a21c4b04c7937253360172eb841d.png)
这是搜索“尤达大师”后Chrome浏览器地址栏中的内容,可见确实是GET无疑。注意:这里浏览器自动把百分号+编码字节转码成中文了。
![293a15f1e06592a4617f24c1db9aa710.gif](https://i-blog.csdnimg.cn/blog_migrate/ce93b73f85f214a6ab0aa16e0dfcbb02.gif)
解析图书馆数据
在前三节中,我们介绍了如何向深图服务器发送一个HTTP请求,以及深图检索图书的逻辑。在这一节中我们将解析服务器响应,从中获取目的信息。
如前文所述,在图书检索系统主页按书名搜索“尤达大师”,实际发送的URL为:
![e1614292c90252fe3bc436e80991408a.png](https://i-blog.csdnimg.cn/blog_migrate/19e36c82fb089b82a40232094a7d681a.jpeg)
乍看上去令人头大。实则:令人头大。
但是,仔细看来,请求信息中只有v_value字段后有%E5%B0%A4……等一段字符编码、v_index被设置为“title”。v_startpubyear,v_publisher,v_author等字段则全部留空。可以推测v_index定义搜索模式,v_author等则是作者、出版社一类的限制信息。据此,可以得到生成请求信息的格式
在请求中可以加入v_library字段指定查找某个特定图书馆的馆藏。经过多次试验,已知的图书馆编号有:
深圳图书馆(本馆):044005
南山图书馆:044006
宝安图书馆:044007
福田图书馆:044008
以上述URL发送请求后,得到的相应全文有1,118行。绝大部分是我们不关心的页面格式、网页逻辑等信息。我们感兴趣的图书信息格式如下:
![c721157f690622ca79b8dfa80d106e56.png](https://i-blog.csdnimg.cn/blog_migrate/e954f23da51431f19f8dd54daa6d7a4c.jpeg)
可见每个搜索结果的主要信息在一个
- 标签和
- [\S\s]*?
为了在VisualStudio编译器中使用,需要对其中的特殊字符进行转义:
- [\\S\\s]*?
以上的表达式匹配的是“booklist”,即书单。若要匹配其中的一本书,需要进一步使用下面的表达式(已转义特殊字符):
[\\s\\S]*?而要匹配每本书的更多信息,又要用到更多的正则表达式:
匹配图书详细信息链接:href=\\\"[\\s\\S]*?\\\"
匹配图书标题:title\\s=\\s\\\"[\\S\\s]*?\\\"
匹配作者:作 者:[\\s\\S]*?<
匹配出版年 :出版年:[\\s\\S]*?<
匹配出版社:出版者:[\\s\\S]*?<
(注意,以上表达式(以斜体区分)中的中文不可删去)
上面的工作只是为了获取图书的基本信息。下面还要对匹配到的详细信息链接进行请求,获取图书在馆情况等资源信息。
根据上面匹配到的详细信息链接,《星球大战神秘事件簿》对应的页面链接为:https://www.szlib.org.cn/Search/searchdetail.jsp?v_tablearray=bibliosm,serbibm,apabibibm,mmbibm,&v_recno=4713184&v_curtable=bibliosm&site=null
细心的读者会发现,这一URL已经包含了GET请求信息,我们不需要对其进行任何修改,只需以此URL发起请求,得到包含详细信息页面HTML文件的服务器响应。本例对应的html文档有829行,大部分依然对我们无用。我们要做的是使用下面给出的正则表达式匹配目标信息:
转义后:
为了帮助理解匹配过程,以下是使用sublime文本编辑器进行匹配的结果:
![4f26325485ed77c3f05043756864e125.gif](https://i-blog.csdnimg.cn/blog_migrate/be622055f6baa49487b41f1dc4a375e3.gif)
可见,每个信息单元(如条形码、索书码、藏书地点、借出信息)都按顺序存储在
标签内。我们只要对这个标签匹配一次,然后在得到的结果列表中筛选各个信息。如果信息量太大开始乱了,没有关系。下面是根据上述方法建构的图书资源模型:
![09937232d0c2a539e4ec3bf517cd0add.png](https://i-blog.csdnimg.cn/blog_migrate/3b25a18831d2bc8fcbbc84d6f5bbaebe.png)
其中Book类模拟一种图书,对应在检索主页搜索得到的图书列表中的一项;BookResource类模拟某种图书中的一本,对应详细信息页面展示的图书资源中的一项。要获取《星球大战神秘事件簿》中一本的馆藏位置,需要四步操作:
获取与“星球大战神秘事件簿”(事实证明也就是与“尤达大师”)相关的图书列表
使用正则表达式匹配在列表中选取一项(本例中只有一项),创建Book类实例
使用正则表达式获得Book类实例指向的详细页面URL
使用正则表达式在详细页面中获得馆藏信息等
至此,解析完毕。
![293a15f1e06592a4617f24c1db9aa710.gif](https://i-blog.csdnimg.cn/blog_migrate/ce93b73f85f214a6ab0aa16e0dfcbb02.gif)
实现查找
由于代码总行数在300行上下,这里不方便直接列出全部代码,仅对部分内容进行解释。要获取完整代码,请访问:
https://github.com/Pieski/PieskiLib(Github页面)
---Program.SearchByName---
根据书名发送URL,并获得图书清单
static void SearchByName(string name){ string path = "https://www.szlib.org.cn/Search/searchshow.jsp?" + "v_tablearray=bibliosm%2Cserbibm%2Capabibibm%2Cmmbibm%2C&" + "v_index=title&v_value=" + name + "+&cirtype=&" + "v_startpubyear=&v_endpubyear=&v_publisher=&v_author=&" + "sortfield=ptitle&sorttype=desc"; if (library != null) path += "&library=" + library; WebRequest request = new WebRequest(path); string htmlinfo = request.Download(); Match booklist = Regex.Match(htmlinfo, "
[\\S\\s]*?"); MatchCollection books = Regex.Matches(booklist.Value, "[\\s\\S]*?"); List result = new List(); foreach (Match match in books) { Book book = new Book(match.Value); book.GetResources(); result.Add(book); } ShowResult(result);}
函数参数为书名。path是请求的URL。如果用户此前设置了图书馆编号,则在URL中加入图书馆字段。
Match是正则匹配类型,MatchCollection是匹配结果类型。
---Book.Book---
Book类的构造函数,根据与此类型有关的html文本段匹配出标题、作者等信息
internal Book(string html){ string ori_link = Regex.Match(html, "href=\\\"[\\s\\S]*?\\\"").Value; string ori_tittle = Regex.Match(html, "title\\s=\\s\\\"[\\S\\s]*?\\\"").Value; string ori_author = Regex.Match(html, "作 者:[\\s\\S]*?).Value; string ori_date = Regex.Match(html, "出版年:[\\s\\S]*?).Value; string ori_publisher = Regex.Match(html, "出版者:[\\s\\S]*?).Value; link = "https://www.szlib.org.cn/Search/" + ori_link.Split('"')[1]; title = ori_tittle.Split('"')[1]; author = ori_author.Replace("作 者:", "").Replace(",""); date = ori_date.Replace("出版年:","").Replace(", ""); publisher = ori_publisher.Replace("出版者:", "").Replace(", "");}
函数参数为与此类型有关的html文本段,是SearchByName函数初步匹配的结果。
---BookResource.BookResource---
BookResouce类的构造函数,根据与此类型有关的html文本段匹配出条形编码号、索书码、借还情况等
internal BookResource(string html){ MatchCollection tdinfos = Regex.Matches(html, "