{"id":13746567,"url":"https://github.com/luohaha/jlitespider","last_synced_at":"2025-07-21T00:34:36.132Z","repository":{"id":57721173,"uuid":"53122238","full_name":"luohaha/jlitespider","owner":"luohaha","description":"A lite distributed Java spider framework :-)","archived":false,"fork":false,"pushed_at":"2017-05-03T13:06:54.000Z","size":10023,"stargazers_count":144,"open_issues_count":2,"forks_count":37,"subscribers_count":18,"default_branch":"master","last_synced_at":"2025-05-13T05:41:10.893Z","etag":null,"topics":["crawler","distributed","distributed-systems","rabbitmq","spider"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/luohaha.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-03-04T09:03:42.000Z","updated_at":"2025-05-06T09:07:52.000Z","dependencies_parsed_at":"2022-09-26T21:41:24.909Z","dependency_job_id":null,"html_url":"https://github.com/luohaha/jlitespider","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/luohaha/jlitespider","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luohaha%2Fjlitespider","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luohaha%2Fjlitespider/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luohaha%2Fjlitespider/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luohaha%2Fjlitespider/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/luohaha","download_url":"https://codeload.github.com/luohaha/jlitespider/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luohaha%2Fjlitespider/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266221419,"owners_count":23894966,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crawler","distributed","distributed-systems","rabbitmq","spider"],"created_at":"2024-08-03T06:00:55.623Z","updated_at":"2025-07-21T00:34:36.095Z","avatar_url":"https://github.com/luohaha.png","language":"Java","funding_links":[],"categories":["Java"],"sub_categories":[],"readme":"# JLiteSpider\n**A lite distributed Java spider framework.**  \n**这是一个轻量级的分布式java爬虫框架**\n\n### 特点\n这是一个强大，但又轻量级的分布式爬虫框架。jlitespider天生具有分布式的特点，各个worker之间需要通过一个或者多个消息队列来连接。消息队列我的选择是[rabbitmq](http://www.rabbitmq.com)。worker和消息之间可以是一对一，一对多，多对一或多对多的关系，这些都可以自由而又简单地配置。消息队列中存储的消息分为四种：url，页面源码，解析后的结果以及自定义的消息。同样的，worker的工作也分为四部分：下载页面，解析页面，数据持久化和自定义的操作。  \n用户只需要在配置文件中，规定好worker和消息队列之间的关系。接着在代码中，定义好worker的四部分工作。即可完成爬虫的编写。\n\n总体的使用流程如下：\n\n* 启动rabbitmq。\n* 在配置文件中定义worker和消息队列之间的关系。\n* 在代码中编写worker的工作。\n* 最后，启动爬虫。\n\n### 安装\n\n\u003e使用maven：  \n\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003ecom.github.luohaha\u003c/groupId\u003e\n  \u003cartifactId\u003ejlitespider\u003c/artifactId\u003e\n  \u003cversion\u003e0.4.3\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n\u003e直接下载jar包:  \n\n点击[下载](http://7xrlnt.com1.z0.glb.clouddn.com/jlitespider-0.4.1.jar)。  \n\n### 设计思想\n\n虽然JLiteSpider将抓取流程抽象成了几个部分，但这并不意味着你就必须遵从这种抽象，你应该根据自己的应用场景，来作出最符合效率最大化的使用决策。比如，如果你抓取的网页源码较大，如果把网页源码也存入消息队列，会导致消息队列负担过大。所以这个时候比较好的做法是将下载和解析的流程合并，直接向消息队列输出解析后的结果。  \n所以，虽然JLiteSpider帮你抽象出了抓取过程中的不同阶段，但这完全是选择性的，用户完全是自由的。我在设计JLiteSpider的时候，尽力保障了自由。后面要介绍到的Worker和消息队列的自由配置，以及添加了`freeman`，同样是这种设计思路的体现。\n\n### Worker和消息队列之间关系\n\nworker和消息队列之间的关系可以是一对一，多对一，一对多，多对多，都是可以配置的。在配置文件中，写上要监听的消息队列和要发送的消息队列。例如：\n\n```json\n{\n    \"workerid\" : 2,\n    \"mq\" : [{\n        \"name\" : \"one\",\n        \"host\" : \"localhost\",\n        \"port\" : 5672,\n        \"qos\" : 3  ,\n        \"queue\" : \"url\"\n    },\n    {\n        \"name\" : \"two\",\n        \"host\" : \"localhost\",\n        \"port\" : 5672,\n        \"qos\" : 3  ,\n        \"queue\" : \"hello\"\n    }],\n    \"sendto\" : [\"two\"],\n    \"recvfrom\" : [\"one\", \"two\"]\n}\n```\n\n\u003eworkerid : worker的id号  \n\u003emq : 各个消息队列所在的位置，和配置信息。`name`字段为这个消息队列的唯一标识符，供消息队列的获取使用。`host`为消息队列所在的主机ip，`port`为消息队列的监听端口号（rabbitmq中默认为5672）。`qos`为消息队列每次将消息发给worker时的消息个数。`queue`为消息队列的名字。`host`+`port`+`queue`可以理解为是消息队列的唯一地址。  \n\u003esendto : 要发送到的消息队列，填入的信息为`mq`中的`name`字段中的标识符。  \n\u003erecvfrom : 要监听的消息队列，消息队列会把消息分发到这个worker中。填入的信息同样为`mq`中的`name`字段中的标识符。\n\n### 消息的设计\n\n在消息队列中，消息一共有四种类型。分别是url，page，result和自定义类型。在worker的程序中，可以通过messagequeue的四种方法(sendUrl, sendPage, sendResult, send)来插入消息。worker的downloader会处理url消息，processor会处理page消息，saver会处理result消息，freeman会处理所有的自定义的消息。我们所要做的工作，就是实现好worker中的这四个函数。\n\n### Worker接口的设计\n\nJLiteSpider将整个的爬虫抓取流程抽象成四个部分，由四个接口来定义。分别是downloader，processor，saver和freeman。它们分别处理上述提到的四种消息。 \n\n你所需要做的是，实现这个接口，并将想要抓取的url链表返回。具体的实现细节，可以由你高度定制。  \n\n#### 1. Downloader:\n\n\u003e这部分实现的是页面下载的任务，将想要抓取的url链表，转化（下载后存储）为相应的页面数据链表。\n\n接口设计如下：\n\n```java\npublic interface Downloader {\n\t/**\n\t * 下载url所指定的页面。\n\t * @param url \n\t * 收到的由消息队列传过来的消息\n\t * @param mQueue \n\t * 提供把消息发送到各个消息队列的方法\n\t * @throws IOException\n\t */\n\tpublic void download(Object url, Map\u003cString, MessageQueue\u003e mQueue) throws IOException;\n}\n```\n\n你同样可以实现这个接口，具体的实现可由你自由定制，只要实现`download`函数。`url`是消息队列推送过来的消息，里面不一定是一条`url`，具体是什么内容，是由你当初传入消息队列时决定的。`mQueue`提供了消息发送到各个消息队列的方法，通过`mQueue.get(\"...\")`选取消息队列，然后执行messagequeue的四种方法(sendUrl, sendPage, sendResult, send)来插入消息。\n\n#### 2. Processor:\n\n\u003e`Processor`是解析器的接口，这里会从网页的原始文件中提取出有用的信息。\n\n接口设计：\n\n```java\npublic interface Processor{\n\t/**\n\t * 处理下载下来的页面源代码\n\t * @param page\n\t * 消息队列推送过来的页面源代码数据消息\n\t * @param mQueue\n\t * 提供把消息发送到各个消息队列的方法\n\t * @throws IOException\n\t */\n\tpublic void process(Object page, Map\u003cString, MessageQueue\u003e mQueue) throws IOException;\n}\n\n```\n\n实现这个接口，完成对页面源码的解析处理。`page`是由消息队列推送过来的消息，具体格式同样是由你在传入时决定好的。`mQueue`使用同上。  \n\n\n#### 3. Saver:\n\n\u003e`Saver`实现的是对解析得到结果的处理，可以将你解析后得到的数据存入数据库，文件等等。或者将url重新存入消息队列，实现迭代抓取。\n\n接口的设计：\n\n```java\npublic interface Saver {\n\t/**\n\t * 处理最终解析得到的结果\n\t * @param result \n\t * 消息队列推送过来的结果消息\n\t * @param mQueue \n\t * 提供把消息发送到各个消息队列的方法\n\t * @throws IOException\n\t */\n\tpublic void save(Object result, Map\u003cString, MessageQueue\u003e mQueue) throws IOException;\n}\n```\n\n通过实现这个接口，可以完成对结果的处理。你同样可以实现这个接口，具体的实现可由你自由定制，只要实现`download`函数。`result`是消息队列推送过来的结果消息，具体的格式是由你当初传入消息队列时决定的。`mQueue`的使用同上。\n\n#### 4. Freeman:\n\n\u003e通过上述的三个流程，可以实现爬虫抓取的一个正常流程。但是`jlitespider`同样提供了自定义的功能，你可以完善，加强，改进甚至颠覆上述的抓取流程。`freeman`就是一个处理自定义消息格式的接口，实现它就可以定义自己的格式，以至于定义自己的流程。\n\n接口的设计：\n\n```java\npublic interface Freeman {\n\t/**\n\t * 自定义的处理函数\n\t * @param key\n\t * key为自定义的消息标记\n\t * @param msg\n\t * 消息队列推送的消息\n\t * @param mQueue\n\t * 提供把消息发送到各个消息队列的方法\n\t * @throws IOException\n\t */\n\tpublic void doSomeThing(String key, Object msg, Map\u003cString, MessageQueue\u003e mQueue) throws IOException;\n}\n```\n\n通过实现`doSomeThing`函数，你就可以处理来自消息队列的自定义消息。`key`为消息的标记，`msg`为消息的内容。同样，通过`mQueue`的`send`方法，可以实现向消息队列发送自定义消息的操作。(需要注意，自定义的消息标记不能为：`url`，`page`，`result`。否则会被认为是`jlitespider`的保留消息，也就是由上述的三个接口函数来处理。)\n\n### 总结说明\n\n`jlitespider`的设计可能会让您有些疑惑，不过等您熟悉这一整套的设计之后，您就会发现`jlitespider`是多么的灵活和易于使用。\n\n###使用方法\n\nJLiteSpider使用：\n\n```java\n//worker的启动\nSpider.create() //创建实例\n      .setDownloader(...) //设置实现了Downloader接口的下载器\n      .setProcessor(...) //设置实现了Processor接口的解析器\n      .setSaver(...) //设置实现了Saver接口的数据持久化方法\n      .setFreeman(...) //设置自定义消息的处理函数\n      .setSettingFile(...) //设置配置文件\n      .begin(); //开始爬虫\n\n//消息队列中初始消息添加器的使用。只有向消息队列中添加初始的消息后，整个爬虫系统才能启动，因此称其为spider的lighter（点火器）。\nSpiderLighter.locateMQ(\"localhost\", 5672, \"MQ's name\") // 定位到要访问的消息队列\n                 .addUrl(...) //向消息队列添加url类型的消息\n                 .addPage(...) //向消息队列添加page类型的消息\n                 .addResult(...) //向消息队列添加result类型的消息\n                 .add(..., ...) //向消息队列添加自定义类型的消息\n                 .close() //关闭连接，一定要记得在最后调用！\n```\n\n以豆瓣电影的页面为例子，假设我们要抓取豆瓣电影的爱情分类中的所有电影名称，并存入txt文件中：   \n\n* 首先，需要设计消息队列和worker之间的关系。我的设计是有两个worker和两个消息队列，其中一个worker在main消息队列上，负责下载，解析并把最终结果传入data消息队列。第二个worker从data消息队列中取数据，并存入txt文件中。两个worker的配置文件如下：  \n\n第一个worker：\n\n```json\n{\n    \"workerid\" : 1,\n    \"mq\" : [{\n        \"name\" : \"main\",\n        \"host\" : \"localhost\",\n        \"port\" : 5672,\n        \"qos\" : 3  ,\n        \"queue\" : \"main\"\n    }, {\n        \"name\" : \"data\",\n        \"host\" : \"localhost\",\n        \"port\" : 5672,\n        \"qos\" : 3  ,\n        \"queue\" : \"data\"\n    }],\n    \"sendto\" : [\"main\", \"data\"],\n    \"recvfrom\" : [\"main\"]\n}\n```\n\n第二个worker：\n\n```json\n{\n    \"workerid\" : 2,\n    \"mq\" : [{\n        \"name\" : \"main\",\n        \"host\" : \"localhost\",\n        \"port\" : 5672,\n        \"qos\" : 3  ,\n        \"queue\" : \"main\"\n    }, {\n        \"name\" : \"data\",\n        \"host\" : \"localhost\",\n        \"port\" : 5672,\n        \"qos\" : 3  ,\n        \"queue\" : \"data\"\n    }],\n    \"sendto\" : [],\n    \"recvfrom\" : [\"data\"]\n}\n```\n\n* 接着，编写第一个worker的代码，如下： \n\n```java\n//下载页面数据，并存入main队列。\npublic class DoubanDownloader implements Downloader {\n\tprivate Logger logger = Logger.getLogger(\"DoubanDownloader\");\n\t@Override\n\tpublic void download(Object url, Map\u003cString, MessageQueue\u003e mQueue) throws IOException {\n\t\t// TODO Auto-generated method stub\n\t\tString result = \"\";\n\t\ttry {\n\t\t\tresult = Network.create()\n\t\t\t\t            .setUserAgent(\"...\")\n\t\t\t\t            .setCookie(\"...\")\n\t\t\t\t            .downloader(url.toString());\n\t\t\t//下载成功，将页面数据放入main消息队列\n\t\t\tmQueue.get(\"main\").sendPage(result);\n\t\t} catch (IOException e) {\n\t\t\tlogger.info(\"本次下载失败！重新下载！\");\n\t\t\t//因为下载失败，所以将url重新放入main队列中\n\t\t\tmQueue.get(\"main\").sendUrl(url);\n\t\t}\n\t}\n\n}\n```\n\n```java\n//解析页面数据，将结果放入main消息队列。同时，后面页面的url信息同样需要放入队列，以便迭代抓取。\npublic class DoubanProcessor implements Processor {\n//url去重复\n\tprivate Set\u003cString\u003e urlset = new HashSet\u003c\u003e();\n\t@Override\n\tpublic void process(Object page, Map\u003cString, MessageQueue\u003e mQueue) throws IOException {\n\t\t// TODO Auto-generated method stub\n\t\tString path = \"//[@id=content]/div/div[1]/div[2]/table/tbody/tr/td[1]/a/@title\";\n\t\tList\u003cString\u003e result = Xsoup.compile(path).evaluate(Jsoup.parse(page.toString())).list();\n\t\t//将结果放入main消息队列\n\t\tmQueue.get(\"main\").sendResult(result);\n\t\tpath = \"//[@id=content]/div/div[1]/div[3]/a/@href\";\n\t\tList\u003cString\u003e url = Xsoup.compile(path).evaluate(Jsoup.parse(page.toString())).list();\n\t\tfor (String each : url) {\n\t\t\tif (!urlset.contains(each)) {\n\t\t\t//如果url之前并未抓取过，则加入main队列，作为接下来要抓取的url\n\t\t\t\tmQueue.get(\"main\").sendUrl(each);\n\t\t\t\turlset.add(each);\n\t\t\t}\n\t\t}\n\t}\n\n}\n```\n\n```java\n//把最终的数据放入data消息队列\npublic class DoubanSaver implements Saver {\n\n\t@Override\n\tpublic void save(Object result, Map\u003cString, MessageQueue\u003e mQueue) throws IOException {\n\t\t// TODO Auto-generated method stub\n\t\tList\u003cString\u003e rList = (List\u003cString\u003e) result;\n\t\tfor (String each : rList) {\n\t\t//把数据发往data消息队列\n\t\t\tmQueue.get(\"data\").send(\"cc\", each);\n\t\t}\n\t}\n\n}\n```\n\n```java\n//启动worker的主程序\npublic class DoubanSpider {\n\tpublic static void main(String[] args) {\n\t\ttry {\n\t\t\tSpider.create().setDownloader(new DoubanDownloader())\n\t\t\t               .setProcessor(new DoubanProcessor())\n\t\t\t               .setSaver(new DoubanSaver())\n\t\t\t               .setSettingFile(\"./conf/setting.json\")\n\t\t\t               .begin();\n\t\t} catch (ShutdownSignalException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (ConsumerCancelledException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (IOException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (TimeoutException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (InterruptedException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (SpiderSettingFileException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n}\n```\n\n* 接下来，还要写第二个worker的代码。\n\n```java\n//接收data消息队列中的数据，写入txt\npublic class SaveToFile implements Freeman {\n\t@Override\n\tpublic void doSomeThing(String key, Object msg, Map\u003cString, MessageQueue\u003e mQueue) throws IOException {\n\t\t// TODO Auto-generated method stub\n\t\tFile file = new File(\"./output/name.txt\");\n\t\tFileWriter fileWriter = new FileWriter(file, true);\n\t\tfileWriter.write(msg.toString() + \"\\n\");\n\t\tfileWriter.flush();\n\t\tfileWriter.close();\n\t}\n}\n```\n\n```java\n//第二个worker的启动主程序\npublic class SaveToFileSpider {\n\tpublic static void main(String[] args) {\n\t\ttry {\n\t\t\tSpider.create().setFreeman(new SaveToFile())\n\t\t\t               .setSettingFile(\"./conf/setting2.json\")\n\t\t\t               .begin();\n\t\t} catch (ShutdownSignalException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (ConsumerCancelledException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (IOException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (TimeoutException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (InterruptedException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (SpiderSettingFileException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n}\n```\n\n* 还要编写一个main消息队列的初始化程序(点火程序)，把第一个入口url放入main消息队列中。\n\n```java\n//把入口url放入main消息队列\npublic class AddUrls {\n\tpublic static void main(String[] args) {\n\t\ttry {\n\t\t\t// 首先定位到要访问的消息队列，队列在localhost:5672/main\n\t\t\t// 然后向这个消息队列添加url\n\t\t\t// 最后关闭lighter\n\t\t\tSpiderLighter.locateMQ(\"localhost\", 5672, \"main\")\n\t\t\t             .addUrl(\"https://movie.douban.com/tag/%E7%88%B1%E6%83%85?start=0\u0026type=T\")\n\t\t\t             .close();\n\t\t} catch (IOException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t} catch (TimeoutException e) {\n\t\t\t// TODO Auto-generated catch block\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n}\n```\n\n* 最后，依次启动程序。启动的顺序是：rabbitmq -\u003e worker1/2 -\u003e 初始化消息程序。关于rabbitmq的使用，它的官方网站上有详细的安装和使用文档，可用于快速搭建rabbitmq的server。\n\n### 辅助工具\n\n\u003e当前版本的`jlitespider`能提供的辅助工具并不多，您在使用`jlitespider`的过程中，可以将您实现的辅助工具合并到`jlitespider`中来，一起来完善`jlitespider`的功能。辅助工具在包`com.github.luohaha.jlitespider.extension`中。\n\n* Network\n\n简单的网络下载器，输入url，返回页面源代码。使用如下：\n\n```java\n\t\tString result = Network.create()\n\t\t\t\t.setCookie(\"...\")\n\t\t\t\t.setProxy(\"...\")\n\t\t\t\t.setTimeout(...)\n\t\t\t\t.setUserAgent(\"...\")\n\t\t\t\t.downloader(url);\n```\n\n\u003e不推荐使用这个网络下载器，因为它是同步的，会阻塞进程。\n\n* AsyncNetwork\n\n异步非阻塞的网络下载器，推荐使用这个作为页面下载器，因为它不会阻塞进程。\n\n```java\n// 创建下载器\nAsyncNetwork asyncNetwork = new AsyncNetwork();\n// 设置cookie\nasyncNetwork.setCookie(cookies);\n// 设置代理\nasyncNetwork.setProxy(\"...\");\n// 设置agent\nasyncNetwork.setUserAgent(\"...\");\n// 启动下载器\nasyncNetwork.begin();\n```\n\n在异步下载器启动后，可以随时往下载器中添加url，和对应的回调处理对象。\n\n```java\n// 添加要下载的页面的url，和下载完成后的处理函数。\nasyncNetwork.addUrl(\"...\", new DownloadCallback() {\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic void onReceived(String result, String url) {\n\t\t\t\t// 下载成功后，执行这个函数。result为下载下来的页面信息，url为对应的url链接。\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t@Override\n\t\t\tpublic void onFailed(Exception exception, String url) {\n\t\t\t\t// 下载失败时，执行这个函数。exception为失败原因。\n\t\t\t\t\n\t\t\t}\n\t\t});\n```\n\n* 解析工具\n\n项目中依赖了两个很常用的解析工具：[xsoup](https://github.com/code4craft/xsoup) 和 [jsoup](https://jsoup.org)。\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluohaha%2Fjlitespider","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fluohaha%2Fjlitespider","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluohaha%2Fjlitespider/lists"}