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

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()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容