爬虫:如何爬取国家行政区划代码

1、前言

因为工作需要,领导让我爬取下国家行政区划代码。本来觉得是件很简单的事,因为看结构,这个还是挺简单的,但是实现起来却发现不是那么回事。

我们先看下页面长什么样子:国家统计局区划代码

页面展示的是省级区划代码,点进去依次是市、县(区)、乡镇、街道区划代码,一共5级。(正常的数据都是5级,其中中山市、东莞市、儋州市这3个特殊,只有4级,需要特殊处理)。

页面结构蛮简单的,就是个级联数据,我这里就不贴图了。

2、爬虫工具

我选用的node+cherrio+puppeteer,puppeteer是一个基于chrome的无界面浏览器。具体使用方法,我这里就不详细介绍了,刚兴趣的可以点这里,教程。

3.思路

刚开始爬取数据,我的想法很简单。

数据是一级级的,我爬取的时候也一级级爬就行(也就是深度遍历)。

我先爬取省页面,然后遍历这些省,获取下一级页面的链接(市页面链接)。然后我再依次打开这些链接,获取到市的数据和下下级页面的链接(县页面链接),我再打开下下级页面链接获取数据和下下下级页面链接,以此类推。

这样等获取完了,我就得到了一个大的json数据,我再把数据存成一个json或者excle。就可以完美交差了。

估计很多小伙伴跟我思路一样,但是这个思路是行不通的。原因有以下两点:

1.程序可调试性太差,数据涉及4层循环,而且每个循环里都是异步操作。有一个地方出错了,整个程序就无法进行。很多时候,你还不知道那个地方出错了。很多时候,运行了半天了,一个错误,导致前面的努力都会白费。

2.数据量很大,我本来以为数据量没多少,实际数据量是几十M。用记事本都打不开,也许你们的电脑可以吧,可怜我的破电脑是打不开的。

综上两点,我们得转变思路。

于是我改用广度遍历的方法。我先爬取省级数据存起来,再爬取市级数据存起来,再爬取县区数据,以此类推。

4.行动

有了思路,我们开始行动,要存储数据,可以选择文件或者数据库,我实际选择了mongo数据库,这里为了演示方便,我改用文件存储。

上面废话了很多,我这里就不废话了,直接贴程序。 文末有完整代码

4.1 打开浏览器

要爬取数据,首先我们要创建一个浏览器,然后用程序控制这个浏览器打开我们想要的页面,从而得到页面内容。

创建浏览器的代码比较复杂,是因为要规避网站的一些反爬虫机制。

const puppeteer = require('puppeteer');
async function openBrowser(){
    let browser = await puppeteer.launch({
      // headless: false,
      args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  let page = await browser.newPage();
  page.evaluateOnNewDocument(() => {
      const newProto = navigator.__proto__;
      delete newProto.webdriver;
      navigator.__proto__ = newProto
      
      window.chrome = {};  //添加window.chrome字段,为增加真实性还需向内部填充一些值
        window.chrome.app = {"InstallState":"hehe", "RunningState":"haha", "getDetails":"xixi", "getIsInstalled":"ohno"};
        window.chrome.csi = function(){};
        window.chrome.loadTimes = function(){};
        window.chrome.runtime = function(){};
        Object.defineProperty(navigator, 'userAgent', {  //userAgent在无头模式下有headless字样,所以需覆写
            get: () => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36",
        });
        Object.defineProperty(navigator, 'plugins', {  //伪装真实的插件信息
            get: () => [{"description": "Portable Document Format",
                        "filename": "internal-pdf-viewer",
                        "length": 1,
                        "name": "Chrome PDF Plugin"}]
        });
        Object.defineProperty(navigator, 'languages', { //添加语言
            get: () => ["zh-CN", "zh", "en"],
        });
        const originalQuery = window.navigator.permissions.query; //notification伪装
        window.navigator.permissions.query = (parameters) => (
            parameters.name === 'notifications' ?
            Promise.resolve({ state: Notification.permission }) :
            originalQuery(parameters)
        )
  })
    return [browser,page]
}

关闭浏览器

async function pageClose(browser){
    await browser.close();
}

4.2 辅助函数

1.引入node模块

const path = require('path')
const cherrio = require('cheerio')
const fs = require('fs')

2.获取浏览器

定义了2个全局变量,browser和page

let [browser, page] = [null, null]
async function createBrowser() {
  [browser, page] = await openBrowser()
}

3.获取页面内容

async function getPageContent(page, url) {
  try {
    await page.goto(url, { 'waitUntil': 'load',timeout:30000 });
    let html = await page.content()
    return html
  } catch (error) {
    console.log('error:',error)    
  }
}

4.获取下一级页面的链接

function getRelativeBaseUrl(url) {
  let lastIndex = url.lastIndexOf('/')
  return url.slice(0, lastIndex)
}

function computedUrl(url, $aItem) {
  // console.log('$aItem:',$aItem)
  if ($aItem.length !== 0) {
    return getRelativeBaseUrl(url) + '/' + $aItem.attr('href')
  } else {
    return undefined
  }
}

5.根据页面内容获取数据

这里用到了cherrio,具体用法点这里,cherrio教程

function getHrefsByContent(url, content) {
  let hrefArr = []
  let $ = cherrio.load(content)
  $('.provincetable a').each((index, item) => {
    // console.log(item)
    
    let $aItem = $(item)
    let nextUrl = computedUrl(url, $aItem)
    // 省级数据是没有code的,我这里取链接地址的数字部分作为code
    let code = path.basename(nextUrl).split('.')[0]
    let text = {
      name: $(item).text(),
      type:'pro',
      curUrl: url,
      nextUrl: nextUrl,
      code:code
    }
    hrefArr.push(text)
  })
  return hrefArr
}

5.将数据存储到文件

function createDataFile(fileName,dataStr,basePath='.') {
  const proPath = path.join('./', basePath);
  const filePath = path.join('./', basePath, fileName);
  const proPathExits = fs.existsSync(proPath);
  if (!proPathExits) {
    fs.mkdirSync(proPath);
  }

  let titles = ['名称','类型','区划代码','当前页面链接','下一级页面链接']

  dataStr = '\uFEFF' + titles.join(',')+'\n' + dataStr
  // 此时路径都已经存在
  fs.writeFileSync(filePath, dataStr, {
    encoding: 'utf-8',
  });
  console.log(`${fileName}创建完成`);
}

4.3 获取省数据

获取省份数据的地址是这个,2021年统计用区划代码和城乡划分代码

//  获取省份的地址 http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2021/index.html
async function getProvinceUrl(url) {
  let content = await getPageContent(page, url)
  let hrefArr = getHrefsByContent(url, content)
  // console.log('getProvinceUrl hrefArr:',hrefArr)
  return hrefArr
}

4.4 存储省级数据

async function saveProData() {
  await createBrowser()
  let proData = await getProvinceUrl('http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2021/index.html')
  // console.log('proData:',proData)
  let proStr = proData.map(pro=>{
    return `${pro.name},${pro.type},${pro.code},${pro.curUrl},${pro.nextUrl}`
  })
  try {
    // 文件存储在data下temp文件夹下,你们可以根据自己的需要,选择不同的存储位置。
    createDataFile('省数据.csv',proStr.join('\n'),path.join('.','data','temp'))
    console.log('插入省份数据完成')
  } catch (error) {
    console.log('重复了')
  }
  pageClose(browser)
}

5.获取省数据

保存省数据的函数是saveProData ,我们把它暴露出来

saveProData()

我代码是保存在temp.js文件里。shell端执行node temp.js,就会执行saveProData函数。

得到文件如下

[图片上传失败...(image-392c9b-1656395453942)]

ok,我们就获取到省级数据了。

6.获取市数据

通过上面的操作,我们获取到了省数据和各省下一级的页面地址(nextUrl),也就是市级页面的地址。 遍历省数据,依次打开市级页面地址,就可以获取到市数据和下一级(县区)页面地址。依次类推,我们就可以获取到所有我们想要的数据。

代码我就不贴了,授人以鱼不如授人以渔。我想我已经把渔的方法说的很清楚了。感兴趣的小伙伴可以自己实现。

7.爬取的结果

本人写这个爬虫,花了3天左右的时间,不想要这么麻烦的小伙伴,可以直接联系我。我可以提供json或excle格式的数据。

前提是我不是免费的哦,爬取的方法免费说了,数据多少要收点辛苦钱。中年人有孩子有家,没办法。但也不贵,只需要一杯咖啡钱(20元)。需要的可以联系我,微信:guo330504。或者扫二维码

如果有爬虫、前端相关的外包也可以找我

[图片上传失败...(image-6d9204-1656395453942)]

本人爬取完整的数据截图如下:

省列表:
[图片上传失败...(image-3f57f8-1656395453942)]

以广东为例,广东数据如下:

[图片上传失败...(image-84a625-1656395453942)]

广州市数据如下:

[图片上传失败...(image-1338d1-1656395453942)]

7.完成代码

爬取省数据完整代码如下:

const path = require('path')
const cherrio = require('cheerio')
const fs = require('fs')


const puppeteer = require('puppeteer');

// 打开浏览器
async function openBrowser(){
    let browser = await puppeteer.launch({
      // headless: false,
      args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  let page = await browser.newPage();
  page.evaluateOnNewDocument(() => {
      const newProto = navigator.__proto__;
      delete newProto.webdriver;
      navigator.__proto__ = newProto
      
      window.chrome = {};  //添加window.chrome字段,为增加真实性还需向内部填充一些值
        window.chrome.app = {"InstallState":"hehe", "RunningState":"haha", "getDetails":"xixi", "getIsInstalled":"ohno"};
        window.chrome.csi = function(){};
        window.chrome.loadTimes = function(){};
        window.chrome.runtime = function(){};
        Object.defineProperty(navigator, 'userAgent', {  //userAgent在无头模式下有headless字样,所以需覆写
            get: () => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36",
        });
        Object.defineProperty(navigator, 'plugins', {  //伪装真实的插件信息
            get: () => [{"description": "Portable Document Format",
                        "filename": "internal-pdf-viewer",
                        "length": 1,
                        "name": "Chrome PDF Plugin"}]
        });
        Object.defineProperty(navigator, 'languages', { //添加语言
            get: () => ["zh-CN", "zh", "en"],
        });
        const originalQuery = window.navigator.permissions.query; //notification伪装
        window.navigator.permissions.query = (parameters) => (
            parameters.name === 'notifications' ?
            Promise.resolve({ state: Notification.permission }) :
            originalQuery(parameters)
        )
  })
    return [browser,page]
}



// 保存文件
function createDataFile(fileName,dataStr,basePath='.') {
  const proPath = path.join('./', basePath);
  const filePath = path.join('./', basePath, fileName);
  const proPathExits = fs.existsSync(proPath);
  if (!proPathExits) {
    fs.mkdirSync(proPath);
  }

  // 此时路径都已经存在
  fs.writeFileSync(filePath, dataStr, {
    encoding: 'utf-8',
  });
  console.log(`${fileName}创建完成`);
}

// 关闭浏览器
async function pageClose(browser){
    await browser.close();
}

// 获取浏览器
let [browser, page] = [null, null]
async function createBrowser() {
  [browser, page] = await openBrowser()
}

// 获取页面内容
async function getPageContent(page, url) {
  try {
    await page.goto(url, { 'waitUntil': 'load',timeout:30000 });
    let html = await page.content()
    return html
  } catch (error) {
    console.log('error:',error)    
  }
}

// 获取下一级页面链接
function getRelativeBaseUrl(url) {
  let lastIndex = url.lastIndexOf('/')
  return url.slice(0, lastIndex)
}

function computedUrl(url, $aItem) {
  // console.log('$aItem:',$aItem)
  if ($aItem.length !== 0) {
    return getRelativeBaseUrl(url) + '/' + $aItem.attr('href')
  } else {
    return undefined
  }
}

// 根据页面内容获取需要的数据
function getHrefsByContent(url, content) {
  let hrefArr = []
  let $ = cherrio.load(content)
  $('.provincetable a').each((index, item) => {
    // console.log(item)
    
    let $aItem = $(item)
    let nextUrl = computedUrl(url, $aItem)
    // 省级数据是没有code的,我这里取链接地址的数字部分作为code
    let code = path.basename(nextUrl).split('.')[0]
    let text = {
      name: $(item).text(),
      type:'pro',
      curUrl: url,
      nextUrl: nextUrl,
      code:code
    }
    hrefArr.push(text)
  })
  return hrefArr
}



// 获取数据
async function getProvinceUrl(url) {
  let content = await getPageContent(page, url)
  let hrefArr = getHrefsByContent(url, content)
  // console.log('getProvinceUrl hrefArr:',hrefArr)
  return hrefArr
}

// 将数据保存为文件
function createDataFile(fileName,dataStr,basePath='.') {
  const proPath = path.join('./', basePath);
  const filePath = path.join('./', basePath, fileName);
  const proPathExits = fs.existsSync(proPath);
  if (!proPathExits) {
    fs.mkdirSync(proPath);
  }

  let titles = ['名称','类型','区划代码','当前页面链接','下一级页面链接']

  dataStr = '\uFEFF' + titles.join(',')+'\n' + dataStr
  // 此时路径都已经存在
  fs.writeFileSync(filePath, dataStr, {
    encoding: 'utf-8',
  });
  console.log(`${fileName}创建完成`);
}

// 保存省数据
async function saveProData() {
  await createBrowser()
  let proData = await getProvinceUrl('http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2021/index.html')
  // console.log('proData:',proData)
  // 插入数据库
  let proStr = proData.map(pro=>{
    return `${pro.name},${pro.type},${pro.code},${pro.curUrl},${pro.nextUrl}`
  })
  try {
    createDataFile('省数据.csv',proStr.join('\n'),path.join('.','data','temp'))
    console.log('插入省份数据完成')
  } catch (error) {
    console.log('重复了error:',error)
  }
  pageClose(browser)
}

saveProData()

你可能感兴趣的:(爬虫:如何爬取国家行政区划代码)