【Howe 学爬虫】全国统计用区划代码爬取
网络爬虫(又称为网页蜘蛛,网络机器人,在 FOAF 社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。
如何开始
做一件事情,难得不是做什么,难得是怎么做,怎么开始。良好的开端成功的一半,下面让我们一起头脑风暴一下,这件事情应该怎么做。
总共有 31 个省,每个省有很多市,每个市有很多县,每个县有很多乡镇,每个乡镇有很多居委会。既然这样,第一反应肯定是循环,遍历。那么接下来,应该怎么循环。先将这个页面的静态页面 get 到,然后将东西保存并获取链接,再然后循环爬取。
想到了就干,但是写完之后碰到了很多问题。
文件怎么保存,总共有 6 级嵌套。
循环结束才可以保存文件,如果循环中抛异常会导致文件损坏。
关于这两个问题,我的解决方法是:
文件夹下保存一个省汇总的 xls,将省的信息保存下来,包括名称、代号、链接。然后创建每个省的文件夹,在这个文件夹中保存一个市汇总的 xls,保存市的名称、代号、链接。以此类推。
先将省列表爬完,期间不去爬市的信息,得到省的链接。然后再用保存下来的链接去获取每个省的市的信息,期间不去管下一级的信息。一次类推,很大程度上杜绝了文件损坏的发生。
代码分析
在详细研究了各个层次的请求参数之后,其中市县乡的爬取可以提取一个方法。我将请求提取了一个方法。
Get 请求方法
代码:
public String GetMethod(URL url, String encode) { BufferedReader in = null; String ret = null; HttpURLConnection conn = null; try { conn = (HttpURLConnection) url.openConnection(); if (conn != null) { conn.setDoOutput(true); conn.setDoInput(true); conn.setRequestMethod("GET"); conn.connect(); } in = new BufferedReader(new InputStreamReader(conn.getInputStream(), encode)); String line = null; while ((line = in.readLine()) != null) { ret += line; } return ret; } catch (Exception e) { e.printStackTrace(); return "Error :" + e.getMessage(); } finally { try { if (in != null) { in.close(); } conn.disconnect(); } catch (final IOException ex) { ex.printStackTrace(); } }}这个方法很简单的 get 请求。但是细心的同学可能注意到了,我给这个方法传了一个 String encode的参数。这就是我遇到的第一个坑,因为网页编码格式是 GB2312 ,而 JAVA 默认格式是 UTF-8,所以没有传这个参数的时候默认获取 UTF-8 的编码信息,存下来之后发现全部汉字变成了乱码。
获取省份信息
代码:
private static String getProvince() { WebUtils webUtils = new WebUtils(); WritableWorkbook book = null; try { File file = new File(parentPath + File.separator + "汇总.xls"); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } book = Workbook.createWorkbook(file); WritableSheet sheet = book.createSheet("省份", 0); WritableFont font = new WritableFont(WritableFont.ARIAL, 12); WritableCellFormat cellFormat = new WritableCellFormat(font); sheet.setColumnView(0, 30); sheet.setColumnView(1, 10); sheet.setColumnView(2, 90); sheet.addCell(new Label(0, 0, "名称", cellFormat)); sheet.addCell(new Label(1, 0, "代码", cellFormat)); sheet.addCell(new Label(2, 0, "链接", cellFormat)); String ret = webUtils.GetMethod(new URL(baseUrl + "index.html"), "GB2312"); Document doc = Jsoup.parse(ret); Elements provincetrEles = doc.getElementsByClass("provincetr"); int i = 1; for (Element element : provincetrEles) { Elements tdEles = element.getElementsByTag("td"); for (Element td : tdEles) { String href = td.select("a").attr("href"); String name = td.select("a").text().trim(); String areaCode = ""; try { areaCode = href.substring(0, href.indexOf(".")); } catch (Exception e) { // TODO: handle exception } if (href != "" && areaCode != null && name != "") { sheet.addCell(new Label(0, i, name, cellFormat)); sheet.addCell(new Label(1, i, areaCode, cellFormat)); sheet.addCell(new Label(2, i, baseUrl + href, cellFormat)); System.out.println("-[Province] " + i + " SAVED:" + name + " - " + href); i++; } } } return file.getAbsolutePath(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (RowsExceededException e) { e.printStackTrace(); } catch (WriteException e) { e.printStackTrace(); } finally { if (book != null) { try { book.write(); book.close(); System.out.println("Complete".toUpperCase()); } catch (WriteException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } return null;}这里面包含了自己写的 GetMethod 方法,用 jxl 来操作 xls 表格,用 Jsoup 来解析 get 下来的静态页面。值得注意的是,一定要记得释放资源。
获取市县乡
这三个获取大同小异,可以提取一个公共方法。
公共方法
private static void creatXls(String path, String name, String url, String className) { WritableWorkbook book = null; try { File file = new File(path + File.separator + "汇总.xls"); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } book = Workbook.createWorkbook(file); WritableSheet sheet = book.createSheet(name, 0); WritableFont font = new WritableFont(WritableFont.ARIAL, 12); WritableCellFormat cellFormat = new WritableCellFormat(font); sheet.setColumnView(0, 30); sheet.setColumnView(1, 30); sheet.setColumnView(2, 90); sheet.addCell(new Label(0, 0, "名称", cellFormat)); sheet.addCell(new Label(1, 0, "统计用区划代码", cellFormat)); sheet.addCell(new Label(2, 0, "链接", cellFormat)); WebUtils webUtils = new WebUtils(); String ret = webUtils.GetMethod(new URL(url), "GB2312"); Document doc = Jsoup.parse(ret); Elements trElements = doc.getElementsByClass(className); int i = 1; for (Element tr : trElements) { Elements tdElements = tr.getElementsByTag("td"); if (tdElements.size() != 0) { String href = tdElements.first().select("a").attr("href"); String areaCode = tdElements.first().select("a").text().trim(); String cityName = tdElements.last().select("a").text().trim(); if (href != "" && areaCode != null && cityName != "") { sheet.addCell(new Label(0, i, cityName, cellFormat)); sheet.addCell(new Label(1, i, areaCode, cellFormat)); sheet.addCell(new Label(2, i, url.substring(0, url.lastIndexOf("/") + 1) + href, cellFormat)); System.out.println(" --[" + className + "] " + i + " SAVED:" + cityName + " - " + areaCode + " - " + href); i++; } } } } catch (Exception e) { e.printStackTrace(); } finally { if (book != null) { try { book.write(); book.close(); } catch (WriteException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }}没什么特别的,跟获取省的差不多。其中传入参数参见下表
String path 上级爬取之后创建的新文件夹路径
String name 这个传不传无所谓,是即将爬的单位名
String url 即将爬取的链接
String className 静态页面中的 class 名
获取市
private static String getCity(File file, String code) throws Exception { if (!file.exists()) { throw new Exception("【Error】文件不存在:" + file.getName()); } try { String[] nameList = readXLS(file, 0, 1); String[] codeList = readXLS(file, 1, 1); String[] urlList = readXLS(file, 2, 1); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } if (code != null) { // TODO 单独获取 int index = Arrays.binarySearch(codeList, code); if (index < 0) { throw new Exception("省份代码不存在"); } String path = file.getParent() + File.separator + nameList[index]; creatXls(path, nameList[index], urlList[index], "citytr"); return path + File.separator + "汇总.xls"; } else { String ret = ""; for (int i = 0; i < codeList.length; i++) { if (nameList[i] != null && nameList[i] != "" && nameList[i] != "null") { String path = file.getParent() + File.separator + nameList[i]; creatXls(path, nameList[i], urlList[i], "citytr"); ret = ret + path + File.separator + "汇总.xls" + SPLITE_SYMBOL; } } return ret; } } catch (Exception e) { e.printStackTrace(); } return null;}获取县
private static String getCountry(File file, String code) throws Exception { if (!file.exists()) { throw new Exception("【Error】文件不存在:" + file.getAbsolutePath()); } try { String[] nameList = readXLS(file, 0, 1); String[] codeList = readXLS(file, 1, 1); String[] urlList = readXLS(file, 2, 1); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } if (code != null) { // TODO 单独获取 int index = Arrays.binarySearch(codeList, code); if (index < 0) { throw new Exception("市代码不存在"); } String path = file.getParent() + File.separator + nameList[index]; creatXls(path, nameList[index], urlList[index], "countytr"); return path + File.separator + "汇总.xls"; } else { String ret = ""; for (int i = 0; i < codeList.length; i++) { if (nameList[i] != null && nameList[i] != "" && nameList[i] != "null") { String path = file.getParent() + File.separator + nameList[i]; creatXls(path, nameList[i], urlList[i], "countytr"); ret = ret + path + File.separator + "汇总.xls" + SPLITE_SYMBOL; } } return ret; } } catch (Exception e) { e.printStackTrace(); } return null;}获取乡镇
private static String getTown(File file, String code) throws Exception { if (!file.exists()) { throw new Exception("【Error】文件不存在:" + file.getName()); } if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } try { String[] nameList = readXLS(file, 0, 1); String[] codeList = readXLS(file, 1, 1); String[] urlList = readXLS(file, 2, 1); if (code != null) { // TODO 单独获取 int index = Arrays.binarySearch(codeList, code); if (index < 0) { throw new Exception("县代码不存在"); } String path = file.getParent() + File.separator + nameList[index]; creatXls(path, nameList[index], urlList[index], "towntr"); return path + File.separator + "汇总.xls"; } else { String ret = ""; for (int i = 0; i < codeList.length; i++) { if (nameList[i] != null && nameList[i] != "" && nameList[i] != "null") { String path = file.getParent() + File.separator + nameList[i]; creatXls(path, nameList[i], urlList[i], "towntr"); ret = ret + path + File.separator + "汇总.xls" + SPLITE_SYMBOL; } } return ret; } } catch (Exception e) { e.printStackTrace(); } return null;}获取居委会
居委会内容比其他的多点,其他的大差不差,但是还是不能跟上面的提取公共方法,起码我不会。但是为了让单独获取和批量获取复用,我还是提取了一个方法。
公共方法
private static void createVillageXLS(File file, String url) { WritableWorkbook book = null; try { String ret = null; book = Workbook.createWorkbook(file); WritableSheet sheet = book.createSheet(file.getName().replace(".xls", ""), 0); WebUtils webUtils = new WebUtils(); ret = webUtils.GetMethod(new URL(url), "GB2312"); WritableFont font = new WritableFont(WritableFont.ARIAL, 12); WritableCellFormat cellFormat = new WritableCellFormat(font); sheet.setColumnView(0, 30); sheet.setColumnView(1, 30); sheet.setColumnView(2, 30); sheet.addCell(new Label(0, 0, "名称", cellFormat)); sheet.addCell(new Label(1, 0, "统计用区划代码", cellFormat)); sheet.addCell(new Label(2, 0, "城乡分类代码", cellFormat)); Document doc = Jsoup.parse(ret); Elements trElements = doc.getElementsByClass("villagetr"); int i = 1; for (Element tr : trElements) { Elements tdElements = tr.getElementsByTag("td"); if (tdElements.size() != 0) { String[] strs = tdElements.text().split(" "); String areaCode = strs[0]; String classifiCode = strs[1]; String villageName = strs[2]; sheet.addCell(new Label(0, i, villageName, cellFormat)); sheet.addCell(new Label(1, i, areaCode, cellFormat)); sheet.addCell(new Label(2, i, classifiCode, cellFormat)); System.out.println( " --[Village] " + i + " SAVED:" + villageName + " - " + areaCode + " - " + classifiCode); i++; } } } catch (Exception e) { e.printStackTrace(); } finally { if (book != null) { try { book.write(); book.close(); } catch (WriteException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }}获取居委会
private static void getVillage(File file, String code) throws Exception { if (!file.exists()) { throw new Exception("【Error】文件不存在:" + file.getName()); } try { String[] nameList = readXLS(file, 0, 1); String[] codeList = readXLS(file, 1, 1); String[] urlList = readXLS(file, 2, 1); String path = null; if (code != null) { int index = Arrays.binarySearch(codeList, code); if (index < 0) { throw new Exception("县代码不存在"); } path = file.getParent() + File.separator + nameList[index]; file = new File(path, "村委会汇总.xls"); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } createVillageXLS(file, urlList[index]); } else { for (int i = 0; i < codeList.length; i++) { if (nameList[i] != null && nameList[i] != "" && nameList[i] != "null") { path = file.getParent() + File.separator + nameList[i]; file = new File(path, "村委会汇总.xls"); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } createVillageXLS(file, urlList[i]); } } } Thread.sleep(DELAY_TIME); } catch (Exception e) { e.printStackTrace(); }}其他方法
静态 changliang
private static String baseUrl = "http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2019/"; private static final String parentPath = System.getProperty("user.dir") + File.separator + "NBS"; private static final int DELAY_TIME = 100;程序入口
public static void run() { Scanner scanner = new Scanner(System.in); System.out.println("欢迎使用"); String code = null; while (true) { System.out.println("请选择爬取方式:"); System.out.println("手动选择爬取 -- 1"); System.out.println("自动全部爬取 -- 2"); System.out.println("请输入编码:(只输入数字)"); code = scanner.nextLine().trim(); if (code.matches("\\d")) { if (code.equals("1") || code.equals("2")) { break; } else { System.out.println("只能输入1或者2,请重新输入"); continue; } } else { System.out.println("输入错误,请重新输入"); } } chooseType(code, scanner); System.out.println("获取完毕,按Enter关闭此软件"); scanner.nextLine(); scanner.close();}方式选择
private static void chooseType(String i, Scanner scanner) { switch (i) { case "1": try { // System.out.println("请按 Enter 继续..."); // scanner.nextLine(); // 获取省 String provinceSum = getProvince(); if (provinceSum == null || provinceSum == "") { System.out.println("加载失败"); return; } // 获取市 File file = new File(provinceSum); String code = topLine(file, scanner); String citySum = getCity(file, code); if (citySum == null || citySum == "") { System.out.println("加载失败"); return; } // 获取县 file = new File(citySum); code = topLine(file, scanner); String countrySum = getCountry(file, code); if (countrySum == null || countrySum == "") { System.out.println("加载失败"); return; } // 获取乡镇 file = new File(countrySum); code = topLine(file, scanner); String townSum = getTown(file, code); if (townSum == null || townSum == "") { System.out.println("加载失败"); return; } // 获取村委会 file = new File(townSum); code = topLine(file, scanner); getVillage(file, code); } catch (Exception e) { e.printStackTrace(); } break; case "2": try { for (int j = 0; j < 3; j++) { if (j > 0) { System.out.println("请再次确认" + (3 - j) + "遍"); } System.out.println("****************************"); System.out.println("***********请注意************"); System.out.println("*******将会全部批量爬取*******"); System.out.println("********会消耗大量时间********"); System.out.println("****爬取过程中会有可能封ip*****"); System.out.println("******所以有可能中途失败*******"); System.out.println("*****如果中途停止,请等待******"); System.out.println("****************************"); System.out.println("*****如果你已了解相关危险******"); System.out.println("********请按Enter继续********"); System.out.println("*****************************"); scanner.nextLine(); } // 获取省 String provinceSum = getProvince(); if (provinceSum == null || provinceSum == "") { System.out.println("加载失败"); return; } File file = new File(provinceSum); String[] citySums = getCity(file, null).split(SPLITE_SYMBOL); for (String city : citySums) { System.out.println("************CITY***************"); file = new File(city); String[] countrySums = getCountry(file, null).split(SPLITE_SYMBOL); for (String country : countrySums) { System.out.println("************COUNTRY***************"); file = new File(country); String[] townSums = getTown(file, null).split(SPLITE_SYMBOL); for (String town : townSums) { System.out.println("************TOWN***************"); file = new File(town); getVillage(file, null); } } } } catch (Exception e) { e.printStackTrace(); } break; }}选择头
private static String topLine(File file, Scanner scanner) throws Exception { String[] nameList = readXLS(file, 0, 1); String[] codeList = readXLS(file, 1, 1); String code = null; while (true) { for (int i = 0; i < codeList.length; i++) { System.out.println(nameList[i] + " -- " + i); } System.out.println("请输入编码:(只输入数字)"); code = scanner.nextLine(); if (code.trim().matches("\\d+")) { return codeList[Integer.parseInt(code)]; } else { System.out.println("输入错误,请重新输入:"); } }}读取 xls 文件
private static String[] readXLS(File file, int lieStart, int hangStart) throws Exception { String[] result; try { FileInputStream fis = new FileInputStream(file); Workbook wb = Workbook.getWorkbook(fis); Sheet sheet = wb.getSheet(0); int hangEnd = sheet.getRows(); result = new String[hangEnd - hangStart + 1]; for (int i = hangStart; i < hangEnd; i++) { result[i] = sheet.getCell(lieStart, i).getContents(); } fis.close(); wb.close(); return result; } catch (Exception e) { e.printStackTrace(); throw new Exception("【异常】" + e.getMessage()); }}如何结束
本来到这里已经结束了,但是我这个人呢,就是想搞一些有意思的。如果我想在一台没装 JDK 的 windows 电脑上运行,可不可行?经过度娘畅游之后,还是被我发现了一些方法。这里只是顺带提一下,有机会以后再讲,或者各位看官也可以自己去找一下。下面我粗略的说一下方法:
这些代码我是用 Vscode 写的(别问为什么不用那么多的 IDEA,因为我主力是安卓并且不喜欢装软件),Maven 项目。但是呢,我又不会用这个导出为可执行 jar 文件。所以搬出了老大哥 Eclipse,用 Eclipse 导出为可执行 Jar 文件。
但是如果电脑没有装 JDK,那不是费了吗。jar 文件执行不了。所有我又找到了一个软件 exe4j 可以将 Jar 文件转为 exe 文件。这样就完美了,转 exe 的时候将 JRE 放进去。这样就可以满足要求了。
------------
废话时间
这玩意儿虽说简单,我再下班时间写,还写了两天。学艺不精啊,还需要多磨练。
加油吧,少年
------------
需要源码可以私信联系我
版权声明: 本文为 InfoQ 作者【Howe】的原创文章。
原文链接:【http://xie.infoq.cn/article/b598ceb6939e58bca1925ba25】。文章转载请联系作者。
Howe
人生苦短,保持前行 2020.05.02 加入
狗币程序员一枚











评论