省市区县乡镇村区域代码爬取 -- 统计局 协程 耗时7个小时

1. 写在前面的话

爬取的这个网站http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2018/index.html有一定的反爬机制,需要使用到代理,才能完全的爬取下来,不然,会遇到,爬取了1000多条数据之后,或出现502的服务器错误。
还有一个注意点:**就是要想完全的爬取几十万的数据,就是不能断网啊。**关于,断点续爬的机制,暂时还没有实现。

来一张图吧,省份的图
省份区域代码

2. 基本思路

  1. 解析页面,提取数据,存入数据库。(多线程)
  2. 它的页面结构都是相似的table表格,所以这里的爬取采用了递归的方式进行的

3. 源代码

# coding=utf-8
from gevent import monkey
monkey.patch_all()
from gevent import pool
from gevent.lock import BoundedSemaphore
from threading import Thread, Lock

from area import Area
from sqldown import Stats

import requests
import re
import time
import random
import gevent


class AreaSpider(object):
    def __init__(self, host="http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2018/"):
        # 请求的入口地址
        self.url = "%sindex.html" % host
        self.host = host
        # 请求头
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"
        }
        # 获取数据库操作句柄
        self.sql = Stats("china")
        self.sql.create_table()
        # 线程锁
        # self.mutex = Lock()
        self.mutex = BoundedSemaphore()
        # 进程池
        self.pool = pool.Pool()
        # 使用的代理ip
        self.proxies = [
            {'http': '118.89.91.108:8888'},
            {'http': '119.41.236.180:8010'},
            {'http': '119.179.129.244:8060'},
            {'http': '116.114.19.211:443'},
        ]
        # 编码
        self.decode = ["gbk", "utf-8"]
        # 市辖区数量
        self.municipal_district = 0

    def my_get(self, url):
        """自定义请求,用来处理一些反爬"""
        # 如果请求发的太快,造成服务器压力太大,会返回502状态码
        # 但好像又不是这样的问题,应该是,限制一定时间内的ip访问次数,所以一到502,切换到代理访问
        # time.sleep(random.randint(1, 3))
        response = requests.get(url, headers=self.headers)
        print("%s [%d]" % (response.url, response.status_code))
        if response.status_code == 502:
            pass_flag = False
            while not pass_flag: # 一直尝试代理,如果通过,则跳出循环
                # 使用代理
                try:
                    # proxy = requests.get("http://127.0.0.1:8080/random").text.split("://")
                    # proxy = random.choice(self.proxies).split("://")
                    # proxies = {proxy[0]: proxy[1]}
                    proxies = random.choice(self.proxies)
                    response = requests.get(url, headers=self.headers, proxies=proxies, timeout=10)
                except:
                    print("使用代理: {} 失败".format(proxies))
                else:
                    pass_flag = True
                    print("使用代理: {} 成功".format(proxies))

            print("%s [%d]" % (response.url, response.status_code))
        return response

    def get_province_urls(self):
        """爬取身份信息,然后根据爬取的个数,调用线程"""
        response = self.my_get(self.url)
        # 解码异常
        html = response.content.decode(self.decode[0])
        area_list = re.findall(r"<td><a href='(\d+\.html)'>(.+?)<br/></a>", html)
        province_list = []
        for province_url, province_name in area_list:
            province_id = re.match(r"(\d+)\.html", province_url).group(1).ljust(12, "0")
            province_list.append([province_url, province_id])
            self.sql.insert_one(Area(province_id, province_name, None))
        return province_list

    def parse(self, pattern, html, pid):
        """根据传进来的pattern解析html页面,并将提取的数据存取数据库"""
        area_list = re.findall(pattern, html) # 解析出来每个元素有三个值 url, id, name
        _area_list = [] # 这里面每个元素只有两个值 url, pid
        for url, id, name in area_list:
            if name == "市辖区":
                id = pid
                self.mutex.acquire()
                self.municipal_district += 1
                self.mutex.release()
            else:
                area = Area(id, name, pid)
                print(area)
                # 多进程操作加锁
                self.mutex.acquire()
                self.sql.insert_one(area)
                self.mutex.release()
            _area_list.append([url, id])
            
        return _area_list

    def spider(self, prefix_url, area_list):
        """递归爬取方法"""
        for url, pid in area_list:
            response = self.my_get("%s%s" % (prefix_url, url))
            html = response.content.decode(self.decode[0])
            # print(html)
            _area_list = self.parse(r"<td><a href='(\d+/\d+\.html)'>(\d+)</a></td><td><a href='\1'>(.+?)</a></td>",
                                html, pid)
            # 匹配成功
            if _area_list:
                # 拿到前缀url
                _prefix_url = re.match(r"(.+?)\d+\.html", response.url).group(1)
                self.spider(_prefix_url, _area_list)
            # 递归终止条件
            else:
                # 这一层的页面没有url了
                html = response.content.decode(self.decode[0])
                # print(html)
                self.parse(r"<tr class='(villagetr)'><td>(\d+)</td><td>\d+</td><td>(.+?)</td></tr>",
                            html, pid)

    def run(self):
        """整个类的运行入口,启用多线程"""
        start_time = time.time()
        province_list = self.get_province_urls()
        # self.spider(self.host, province_list)
        # 使用多线程
        thread_list = []
        i = 0
        for province in province_list:
            # 这里的第二个参数是一个地区列表
            # 因为页面结构都是一个 table 列表
            i += 1
            print("协程: %d"%i)
            # t = Thread(target=self.spider, args=(self.host, [province]))
            self.pool.apply_async(self.spider, args=(self.host, [province]))
            # thread_list.append(t)
        # 等待子线程(进程)结束
        # for t in thread_list:
        #     t.join()
        #     print("一个省市爬取完成")
        # 关闭进程池
        # self.pool.close()
        # 阻塞等待线程结束
        try:
            self.pool.join()
        except:
            pass

        use_time = time.time() - start_time
        print("spider use time %.2fs = %.2fm = %.2fh 市辖区: %d"%(use_time, use_time/60, use_time/3600, self.municipal_district))


if __name__ == "__main__":
    area_spider = AreaSpider()
    area_spider.run()

4. 后序

这里面有两个模块①area模型②sqldown数据操作api请自己实现。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值