最近有个爬取各国*信息的奇怪需求,要求百度和维基两种版本的数据,最要命的还要保持数据的结构不变。正好印象中隐约记得维基有专门的*列表页,不考虑爬取下来的格式不变的话应该很好爬的样子。
首先思路是通过列表页把每个*的信息页链接爬取下来,然后再逐个去解析信息页就OK了,思路很简单。
那么准备好爬取入口,在wiki上有一个各国*信息的列表页:https://zh.wikipedia.org/wiki/各国*列表
打开这个页面是这样的:
简直就是专为爬取设计的入口页,看了下页面代码结构,也是好爬的不行,查看具体的人物信息页链接是这样的格式:
https://zh.wikipedia.org/wiki/+名字
也就是这一页爬名字统统爬取下来,再凑好链接,加入任务列表就好了。
这是爬取列表页生成任务队列的代码:
/** * 1.拿到列表页,得到所有*的姓名 * 2.用姓名拼凑出链接,加入爬取队列 */ //得到信息列表并遍历列表 List<Selectable> peopleTable = page.getHtml().xpath("//div[@id='mw-content-text']/div/table").nodes(); for (Selectable table : peopleTable) { if (count != 0) { break; } //拼凑链接加入爬取队列 List<Selectable> peopleLine = table.xpath("//tr").nodes(); for (Selectable line : peopleLine) { String name = line.xpath("//td[3]/a/text()").get(); if (null == name || "".equals(name)) { continue; } String country = line.xpath("//td[1]/a/text()").get(); //System.out.println("名字:"+name+"-------------"+"链接:"+url); countrys.put(name, country); String url = "https://zh.wikipedia.org/wiki/" + name; page.addTargetRequest(url); } }
接下来就去人物信息页查看页面结构,发现人物信息页的数据结构不太好爬,是这样子的:
全是p标签和h标签的结构,看着脑袋疼,不过发现了一个有意思的东西,就是页面中有个这样的目录:
那么就根据目录来定位内容进行爬取,正好保证了数据的结构不变。
仔细查看目录发现目录跟后面的内容还是有联系点的:
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
很明显的联系,那就根据目录来爬取了。
在这之前需要先设计好数据,考虑到之后可能不止存*的信息而且每个人物的目录层次变动性很大,所以使用mongodb存数据
设计表如下,尽可能把页面上的数据都朗阔进去,虽然不一定都爬下来:
接下来就开始写解析代码将数据抽取出来了,这一步需要用到jsoup来解析页面,里面有个很好用的方法就是定位到标签后可以拿到它的前一个或后一个兄弟标签,非常适用于这种文本段落没明显分层的页面。下面的是解析代码:
/** * 1.解析详细人物信息页 * 2.编写抽取规则,进行数据抽取 */ leader = new Leader(); //从链接得到所解析人物的姓名,再得到国籍 String leaderUrl = page.getUrl().get(); String leaderName = leaderUrl.replace("https://zh.wikipedia.org/wiki/", ""); String leaderCountry = countrys.get(leaderName); System.out.println(leaderName + "--------" + leaderCountry); leader.setId("" + count); leader.setName(leaderName); leader.setNationality(leaderCountry); leader.setType(new String[] { "军事", "政治" }); // testa = leaderName; //解析页面,得到页面的导航目录,通过目录爬取具体内容 List<Selectable> lists = page.getHtml().xpath("//div[@id='toc']/ul/li").nodes(); List<Item> leaderDetail = new ArrayList<>(); for (Selectable list : lists) { Item item = new Item();//一级目录 List<SubItems> subItems = new ArrayList<SubItems>();//二级目录 List<Selectable> secondLists = list.xpath("//ul//li").nodes(); //如果一级目录下记录不为0,说明当前目录下存在二级标题。否则只存在一级标题 if (secondLists.size() != 0) { String firstItem = list.xpath("//span[2]/text()").get(); //抽取二级目录数据 for (Selectable secondList : secondLists) { SubItems subItem = new SubItems(); String[] secondItem = getText(secondList, page); subItem.setName(secondItem[0]); subItem.setValue(secondItem[1]); subItems.add(subItem); } item.setName(firstItem); item.setValue(""); item.setSubItems(subItems); leaderDetail.add(item); } else { //抽取一级目录数据 String[] secondItem = getText(list, page); item.setName(secondItem[0]); item.setValue(secondItem[1]); leaderDetail.add(item); } }
其中的二级目录内容获取方法:
public String[] getText(Selectable select, Page page) { String itemId = select.xpath("//a/@href").get(); itemId = itemId.replace("#", ""); Document doc = Jsoup.parse(page.getHtml().get()); String citiao = doc.getElementById(itemId).text(); Element elt = doc.getElementById(itemId).parent(); StringBuffer sb = new StringBuffer(); while (true) { elt = elt.nextElementSibling(); if (elt == null || "h2".equals(elt.tagName()) || "h3".equals(elt.tagName())) { break; } sb.append(elt.text()).append("\r\n"); } return new String[] { citiao, sb.toString() }; }
这里面主要的就是要保持爬取下来的数据要按目录结构存储,一级目录包含二级目录,对应关系要把持住,在设计数据库的时候就需要考虑到。
然后是一些相关bean:
public class Leader { private String id; private String name; private String gender; private String nationality; private Date birthday; private String birthPlace; private String[] type; private List<Item> info; private List<Item> details; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; }
public class Item { private String name; private String value; private List<SubItems> subItem; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public List<SubItems> getSubItems() { return subItem; } public void setSubItems(List<SubItems> subItem) { this.subItem = subItem; } }
public class SubItems { private String name; private String value; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } }
最后测试的时候,发现一个很严重的问题,就是有些叫不出名字的国家,他们的*页面没有目录(就寥寥几句简介)。。。。。。导致信息爬取不出。
这是最后无目录的*个数:
这是数据库的数据:
只能说成功了一半。~~|