项目中有一个需求,需要与民政部的数据实时同步。
思路:民政部官网的数据不会直接告诉我们,需要我们去官网查看,每个月民政部会更新两条href链接,是县级一下或者县级以上的两条数据,人工手动定期拷贝略显的low了,使用爬虫来获取网页数据,再将数据经过筛选存入数据库,开启一起定时任务,定时获取网页中最新的两条数据;
实现:所需要的数据,民政部网址是 http://www.mca.gov.cn/article/sj/tjbz/a/2017/
利用爬虫框架 webmagic,抓取这个链接下的数据,只需要抓取a标签下的href内容,这样就能得到我们想存入数据库中的页面内容
利用webmagic框架中有的一个方法“xpath”,进行一个xpath的模糊查询语句进行抓取出指定内容,这个模糊语句表示抓取这个数据中title包含县一下的数据,用String返回一条就可以,这肯定是最新的一条
if (page.getUrl().toString().matches(".*2017/")){ String link = page.getHtml().xpath("//a[@class='artitlelist'and contains(@title,'县以下') ").links().get(); page.addTargetRequest(link); }
这就获取页面中href标签里的内容,但是有些页面的href链接不完整,就要自己手动将抓取的链接加上域名补全,这个可以做个判断实现的;会发现在最外层有一个if判断,这个判断的意思是如果抓取的链接中满足某个条件就返回true,在执行这个条件要做的事情,而后用webmagic中的addTargetRequest方法,将抓取到的链接进行抓取,也就是二次抓取
后来发现一个问题,这个href抓取的链接是shtml格式的,不是传统的html,且点进去这个shtml之后,页面的URL会有变化,这下怎么办呢?经过多番观察测试,发现这个跳转是在shtml中的js里中的事情,在页面中看不到,但是将shtml全部抓取过来之后能看到js里的内容,有个window.location.href,跟在=号后面的才是真实地址,既然知道这个就好办了,照旧第一次抓取循环两次得到shtml链接,将链接放入二次抓取,用选择器将script中的内容提取出来经过筛选组成
else if (page.getUrl().toString().matches(".*shtml")){ String url = "http://www.mca.gov.cn/" + page.getHtml().css("body > script").get().replaceAll("<[^>]+>|window.location.href=|\"|;|\n","").trim(); System.out.println(url); page.addTargetRequest(url); }
webmagic框架可以用$方法,css方法,xpath方法,抓取想要的内容,而后用字符串替换正则,得到真实地址拼接,这个else if跟上一个一样,判断哪个URL抓取进来的数据,而做哪些事,之前是循环两次抓取两条,这里会进来两次,跟循环一样,所以这里用get方法获取单条即可,在调用addTargetRequest方法,将拼接的链接进行第三次抓取
else if (page.getUrl().toString().matches(".*html")){ List<String> data = page.getHtml().xpath("table/tbody/tr/tidyText()").all();
再做第三次判断,这次进来的是不是html,是的话,正式开始抓取表单中的县以下数据然后存放到List里
县级以下的内容有多列,而且还有不要的内容,但是抓取的时候他是不会管你这些的,他只负责抓取,那数据处理还是要自己来
String[] strArr = data.toString().substring(data.toString().indexOf("序号"), data.toString().lastIndexOf("注")).split(",\\s*|\t|\r|\n"); List strArrList = new ArrayList(); for (int i = 0; i < strArr.length; i++) { //放入list中 strArrList.add(strArr[i]); } //放入list中方便删除不要的内容下标 strArrList.remove(0); List<List> lists = new ArrayList<>(); //两层循环,根据Html表单下标获取数据 for (int i = 6; i < strArrList.size() + 6; i++) { List<String> list = new ArrayList<>(); for (int j = 1; j < 8; j++) { String rowSpan = page.getHtml().xpath("table/tbody/tr[" + i + "]/td[" + j + "]").get(); if(rowSpan != null){ list.add(rowSpan); } } lists.add(list); } List dataList = new ArrayList(); //根据上一次循环数据获取的List进行做二层循环取出表单列的数据 for (int i = 0; i < lists.size(); i++) { List newData = new ArrayList(); for (int j = 1; j < 7; j++) { String htmlText = lists.get(i).get(j).toString(); //判断是否包含Html rowspan属性 if (htmlText != null && htmlText.contains("row")) { //将rowspan中的文本取出 String text = htmlText.replaceAll("<[^>]+>", ""); //将rowspan中的数字取出 Integer num = getSpanNumber("rowspan", htmlText); //根据取出的数字循环 if (j == 6) { for (int k = 1; k < num; k++) { //根据rowspan中的文本内容对下一级补全 lists.get(i + k).add(text); } } else { for (int k = 1; k < num; k++) { //根据rowspan中的文本内容对下一级补全 lists.get(i + k).add(j, text); } } } newData.add(htmlText); } dataList.add(newData); } Map<String, Object> map = new HashMap<>(); //取出整理过的数据 try { for (int i = 0; i < lists.size(); i++) { String newData = dataList.get(i).toString().replaceAll("<[^>]+>|\\[|\\]", ""); String[] strings = newData.split(","); map.put("原区划代码", strings[0]); map.put("原名称", strings[1]); map.put("变更原因", strings[2]); map.put("现区划代码", strings[3]); map.put("现名称", strings[4]); map.put("批准文件", strings[5]); System.out.println(map); } } catch (IndexOutOfBoundsException e) { System.out.println(e.getMessage()); } } }
代码看着复杂,实际上理解之后不难看懂,将List里面的数据返回字符串类型,经过字符串的subString方法进行分割,再利用String的indexOf以及lastIndexOf获取输入字符串的坐标数字,好交给subString精确分割,而后利用字符串的split切割方法,用指定正则,去除逗号换行等,返回到数组中,这样就达到了将抓取到的数据分行的效果,再将数组内容取出存放到list中,利用ArrayList的remove方法删除序号这一行,只要表单内容,之前说过数据不完整,要自己手动补全,想了好几天,唯一补全的办法只有一个,那就是利用html标签属性的下标,用两层循环第几行第几列的数据取出,不经过去除html标签筛选,存放到list中,再放入大的list里,对后续进行操作,根据这个大的list大小进行循环取出,取出后再做一层循环取出列的内容,判断这个表单中是否有rowspan这个属性,用到了contains这个方法,有的话进行筛选,筛选出这个rowspan的数字进行循环,下一列补个null的方式将下一个进行存入list中,再存入大的list中,这样小的list就是每一行的内容,大的就是包含了每个行的内容,再将大的list循环取出进行筛选就可以了。
完整代码:
import us.codecraft.webmagic.Page; import us.codecraft.webmagic.Site; import us.codecraft.webmagic.Spider; import us.codecraft.webmagic.processor.PageProcessor; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class reptileTest implements PageProcessor { private Site site = Site.me().setRetryTimes(3).setSleepTime(1000); @Override public void process(Page page) { //第一次抓取 if (page.getUrl().toString().matches(".*2017/")){ String link = page.getHtml().xpath("//a[@class='artitlelist'and contains(@title,'县以下') and contains(@title,'11') ").links().get(); if (link.contains("www")){ page.addTargetRequest(link); }else { String newLink = "http://www.mca.gov.cn" + link; page.addTargetRequest(newLink); } } //第二次抓取 else if (page.getUrl().toString().matches(".*shtml")){ String url = page.getHtml().css("body > script").get().replaceAll("<[^>]+>|window.location.href=|\"|;|\n","").trim(); if (url.contains("www")){ page.addTargetRequest(url); }else { String newUrl = "http://www.mca.gov.cn" + url; page.addTargetRequest(newUrl); } } //第三次正式抓取表单内容 else if (page.getUrl().toString().matches(".*html")){ //获取表单内容 List<String> data = page.getHtml().xpath("table/tbody/tr/tidyText()").all(); //对获取的表单内容进行subString取出不要的内容 String[] strArr = data.toString().substring(data.toString().indexOf("序号"), data.toString().lastIndexOf("注")).split(",\\s*|\t|\r|\n"); List strArrList = new ArrayList(); for (int i = 0; i < strArr.length; i++) { //放入list中 strArrList.add(strArr[i]); } //放入list中方便删除不要的内容下标 strArrList.remove(0); List<List> lists = new ArrayList<>(); //两层循环,根据Html表单下标获取数据 for (int i = 6; i < strArrList.size() + 6; i++) { List<String> list = new ArrayList<>(); for (int j = 1; j < 8; j++) { String rowSpan = page.getHtml().xpath("table/tbody/tr[" + i + "]/td[" + j + "]").get(); if(rowSpan != null){ list.add(rowSpan); } } lists.add(list); } List dataList = new ArrayList(); //根据上一次循环数据获取的List进行做二层循环取出表单列的数据 for (int i = 0; i < lists.size(); i++) { List newData = new ArrayList(); for (int j = 1; j < 7; j++) { String htmlText = lists.get(i).get(j).toString(); //判断是否包含Html rowspan属性 if (htmlText != null && htmlText.contains("row")) { //将rowspan中的文本取出 String text = htmlText.replaceAll("<[^>]+>", ""); //将rowspan中的数字取出 Integer num = getSpanNumber("rowspan", htmlText); //根据取出的数字循环 if (j == 6) { for (int k = 1; k < num; k++) { //根据rowspan中的文本内容对下一级补全 lists.get(i + k).add(text); } } else { for (int k = 1; k < num; k++) { //根据rowspan中的文本内容对下一级补全 lists.get(i + k).add(j, text); } } } newData.add(htmlText); } dataList.add(newData); } Map<String, Object> map = new HashMap<>(); //取出整理过的数据 try { for (int i = 0; i < lists.size(); i++) { String newData = dataList.get(i).toString().replaceAll("<[^>]+>|\\[|\\]", ""); String[] strings = newData.split(","); map.put("原区划代码", strings[0]); map.put("原名称", strings[1]); map.put("变更原因", strings[2]); map.put("现区划代码", strings[3]); map.put("现名称", strings[4]); map.put("批准文件", strings[5]); System.out.println(map); } } catch (IndexOutOfBoundsException e) { System.out.println(e.getMessage()); } } } @Override public Site getSite() { return site; } /** * 自定义方法 * @param span 传入rowspan或者colspan * @param htmlText 传入html代码 * @return 分割指定内容返回 */ public Integer getSpanNumber(String span, String htmlText){ String[] split = htmlText.split(span + "=\""); split = split[1].split("\""); return Integer.parseInt(split[0]); } public static void main(String[] args) { Spider.create(new reptileTest()).addUrl("http://www.mca.gov.cn/article/sj/tjbz/a/2017/").thread(5).start(); } }