1. 写在前面的话
爬取的这个网站http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2018/index.html有一定的反爬机制,需要使用到代理,才能完全的爬取下来,不然,会遇到,爬取了1000
多条数据之后,或出现502
的服务器错误。
还有一个注意点:**就是要想完全的爬取几十万的数据,就是不能断网啊。**关于,断点续爬的机制,暂时还没有实现。
来一张图吧,省份的图
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
请自己实现。。。。