爬虫实战:全国电动汽车充电站数据
项目详情页请访问 Github,喜欢的话就去加个 star 吧 ,附上Github个人博客
先放张效果图,吸引一下眼球
![](https://i-blog.csdnimg.cn/blog_migrate/ce52cca2fb84388765df3297115bbef4.png)
![](https://i-blog.csdnimg.cn/blog_migrate/add9362942bbd4e6e3b8123d6d3c266d.png)
下面进入正题~
想必大家某些时候总需要爬取一些数据,那如何使用 python 简单的实现一个爬取任务呢?
还在为机器学习的数据集而困恼吗?还在为建模时没有数据而烦闷吗?
这里就通过一个实例来实现一个简单的 Web 爬虫程序,首先介绍一些常见的问题:
静态网页爬取 VS 动态网页爬取
静态网页爬取即是网页的 html 文件即包含了你所需要的所有数据并且你可以直接请求到,这通常便有两种方法:
- 正则表达式
- BeautifulSoup
而动态网页则往往需要加载一段时间或者内容需要由ajax 异步更新等等,本处爬取的网站应该算最简单的需要加载一段时间吧~
正则表达式
既然能够直接从 html 源码中获得数据,那么直接把源码请求下来不就好了!通常使用如下代码请求一个静态网页的源码:
def get_one_page(url):
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) ''Chrome/51.0.2704.63 Safari/537.36'}
response = requests.get(url, headers=headers, timeout=30)
if response.status_code == 200:
return response.text
else:
print(response.status_code)
return None
except:
print('访问 http 发生错误... ')
return None
源码获得后便可以直接通过正则表达式匹配所需要的数据了,具体使用方法这里就不多说了,网上教程一抓一大把,这里给个示例:
pattern_fast = re.compile('快充数量:(.*?)个', re.S)
pattern_low = re.compile('慢充数量:(.*?)个', re.S)
kuaichong = re.findall(pattern_fast, str(charge[0]))
manchong = re.findall(pattern_low, str(charge[0]))
上述简要代码将匹配字符串
charge[0]
中的快充桩和慢充桩数据
但是对整个文档进行正则匹配显然不是一件高效的事情,而且某些情况下正则表达式并不是很好写,这时结构化的 html 便有用了,通常使用 BeautifulSoup
来实现 html 的解析与操作
必要的情况也可以缩小目标范围后再使用正则表达式
BeautifulSoup
具体使用方法可直接参考官方文档 ,下面这个示例给出按类解析省份的功能
# 按类(class)解析 html 文件得到所有省份名称
soup = BeautifulSoup(all_html, "lxml")
province = soup.find_all(name='td', attrs={"class": "sel-city-td-sf"})
BeautifulSoup 看上去比暴力正则表达式便要柔和很多,因为我们所感兴趣的数据往往具有相同的类 class 或者 ID,并且 id
在 html 中是唯一的,这样提取一个数据显然十分方便。
如果你以为到这便可以爬取大部分网页那就太天真了,很多数据对提供商来说是不想传播开来的,这时变会有相应的反爬机制,这里手段很多,此博客仅介绍本项目遇到的问题
通常的 Request 请求方式是请求一次成功后便马上将源码下载下来,然而此时页面可能还未加载完全!,通常会是一些 Ajax
异步的 javascript
程序(本项目似乎仅仅是加载慢而已,,),那么这时下载下来的 html 源码可能是没有你想要的信息的。解决方案显然也很清晰,那我就等呀!而实现这种功能的途径往往是 webdriver
Selenium + Webdriver
详细的概念这里就不介绍了,我们需要明确的是 Selenium Python
提供了一个简单的API 便于我们使用 Selenium WebDriver
编写 功能/验收测试。直观上说就是操作一个浏览器的行为,可以实现等待、跳转等等操作,下面我们直接从实例出发分析如何利用这些技术
开始爬取
首先来分析下目标网页,采集的网页来自于北汽,但是地图爬虫不是这次的重点
其实只是不会而已
我们爬取的主要是表单数据,于是我们从这个页面入手
[外链图片转存失败(img-bojExydy-1567002028920)(https://i.loli.net/2018/12/09/5c0d0c98241e3.png)]
根据上图确定如下的思路
-
先获得所有省份的详情页地址
这一步由于省份其实并不多,可以自己建立一个字典存放省份名称,但是傲娇的作者怎么能忍呢?当然要爬!而且这些地址均在同一个类(
sel-city-td-sf
)中 -
针对每个省份从左上角获得电站数量、快充数量、慢充数量的信息
访问网页便会知道这些数据似乎是在加载完地图之后才加载的的,直接使用 Request 会失效
-
针对每一个省份从左边的表单遍历所有的充电站获得每一个充电站的详细数据
这算是最重要的一步,也是最难的一步,我们发现所有的侧边表单的网址都是一样的! ,也就是说必须是从安徽省的网页中点击侧边栏才能跳转到安徽省的充电桩详情页,具体实现见后文
实现前先放一张全国的分布图(虽然数据少,但绝不是手工记录,,,),下面进入正题!
[外链图片转存失败(img-07vkO2d6-1567002028924)(https://i.loli.net/2018/12/15/5c14dfa87fdc8.png)]
关键模块
省份名称及详情页
上述已经提到了通过如下代码便可以提取到所有省份的名称
# 打开起始页面
url = 'http://www.bjev520.com/jsp/beiqi/pcmap/do/index.jsp'
all_html = get_one_page(url)
# 按类(class)解析 html 文件得到所有省份名称
model = 'http://www.bjev520.com/jsp/beiqi/pcmap/do/pcMap.jsp?chargingTypeId=&companyId=&chargingBrandId=&brandStatuId=&cityName='
soup = BeautifulSoup(all_html, "lxml")
province = soup.find_all(name='td', attrs={"class": "sel-city-td-sf"})
不过这还有个小问题,进入详情页的时候需要对网址中的中文进行编码
from urllib.request import quote
quote("安徽省")
# output
# '%E5%AE%89%E5%BE%BD%E7%9C%81'
省份概览信息
进过上述步骤已经可以进入任意省份的详情页,下一步便是从左上角获得电站数量、快充数量、慢充数量的信息,一开始我错以为这三个 id
的数据是 ajax
异步加载的,后来才发现是单线程的,,先分析下这三个 id
的 javascript
代码
/** 修改数量统计 */
function editCharging(dianzhan,kuaichong,manchong){
$("#dianzhan").html(dianzhan);
$("#kuaichong").html(kuaichong);
$("#manchong").html(manchong);
}
这里默认大家会使用浏览器的开发者模式检查源代码,,推荐 chrome F12
这就是一段简单的 js 代码,但是也需要服务器进行响应,加载完毕需要一定时间,那么此时 Request 直接请求一次返回源码便失效了,来看下效果:
all_html = get_one_page("http://www.bjev520.com/jsp/beiqi/pcmap/do/pcMap.jsp?chargingTypeId=&companyId=&chargingBrandId=&brandStatuId=&cityName=%E5%AE%89%E5%BE%BD%E7%9C%81")
soup = BeautifulSoup(all_html, "lxml")
print(soup.find(id="dianzhan"))
# output
# <span id="dianzhan">0</span>
可以看出解析出来的是 0
这时便可以依靠 Webdrive 来实现了,这个模拟的浏览器默认是在页面加载完全才会返回信息的,通过以下代码可以简单实现此功能
driver = webdriver.Chrome("chromedriver.exe")
driver.get("http://www.bjev520.com/jsp/beiqi/pcmap/do/pcMap.jsp?chargingTypeId=&companyId=&chargingBrandId=&brandStatuId=&cityName=%E5%AE%89%E5%BE%BD%E7%9C%81")
try:
# element = WebDriverWait(driver, 10).until(
# EC.presence_of_element_located((By.ID, "dianzhan"))
# )
total = driver.find_element_by_id('dianzhan').text
print(total)
finally:
driver.quit()
# output
# 597
上面注释了一部分代码,WebDriverWait
集成了 Webdrive 的等待事件,上述代码
EC.presence_of_element_located((By.ID, "dianzhan"))
就是直到页面中出现了 dianzhan
这个 id
后方才进行抓取,这在网站数据经由 ajax 异步更新的时候格外有用,官方提供了很多种等待事件可参考 selenium-python中文文档
title_is
title_contains
presence_of_element_located
visibility_of_element_located
visibility_of
presence_of_all_elements_located
text_to_be_present_in_element
等等
ajax 异步更新爬取实例
由于这个项目中用不到 webdrive 的这个机制,感觉有点委屈了它,所以咱么看一个 pythonscraping 给的 ajax 爬取实例
这个网站中单线加载完毕后会通过 ajax 更新段落内容,如果不使用等待事件的话
driver = webdriver.Chrome("chromedriver.exe")
driver.get("http://pythonscraping.com/pages/javascript/ajaxDemo.html")
try:
# element = WebDriverWait(driver, 10).until(
# EC.presence_of_element_located((By.ID, "content"))
# )
total = driver.find_element_by_id('content').text
print(total)
finally:
driver.quit()
# output
# This is some content that will appear on the page while it's loading. You # don't care about scraping this.
返回的就是还没来得及更新的的段落内容,查看网页源码如下
<div id="content">Here is some important text you want to retrieve! <p></p><button id="loadedButton">A button to click!</button></div>
$.ajax({
type: "GET",
url: "loadedContent.php",
success: function(response){
setTimeout(function() {
$('#content').html(response);
}, 2000);
}
});
function ajax_delay(str){
setTimeout("str",2000);
}
从 $.ajax
可以看出这是实打实的异步更新,但是也很容易发现当页面加载完之后会有一个新的 id
——loadedButton
,只要执行等待事件直到这个 id
出现就能抓取到所需内容了
driver = webdriver.Chrome("chromedriver.exe")
driver.get("http://pythonscraping.com/pages/javascript/ajaxDemo.html")
try:
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "loadedButton"))
)
total = driver.find_element_by_id('content').text
print(total)
finally:
driver.quit()
# output
# Here is some important text you want to retrieve!
# A button to click!
嵌套网页(iframe)抓取
抓取页面的侧边表单(充电站详细数据)时发现解析的网页源码没有相关的内容,过了很久后才发现这是因为嵌套的缘故
不进入子 iframe 的话是无法访问其中的html 结构的
因为 HTML 是一个树的结构,只有一个顶点,存在嵌套相当于有多个顶点
马上容易想到这个 iframe 不是给出链接了吗?直接访问呀!这就有反爬机制了,从不同的页面访问这个网址返回的是不一样的 ,这当时就刷新了我的三观,顿时无奈。。
好在后面看到了 webdriver 的另一神奇功能 switch_to
,问题迎刃而解
这个故事告诉我们不全面了解一门技术总会遇到坑,,老手勿喷
解决方案如下:
# 调转到嵌入的 iframe
driver.switch_to.frame('left') # iframe标签的name属性 最重要的一步
soup = BeautifulSoup(driver.page_source, "html.parser")
# 读取每一个充电站
status = soup.find_all('a')
然后就能得到一个个静态网页的链接了 ,他们都长这样
顿时感觉手里多了不知道多少的数据,,,
正则匹配获取充电桩信息
这个比较简单,就不细说了
# 充电站充电桩
charge = soup_location.find_all(
name='div', attrs={"class": "news-c"}) # 由于部分只有快充或者慢充,正则匹配似乎比较方便
pattern_fast = re.compile('快充数量:(.*?)个', re.S)
pattern_low = re.compile('慢充数量:(.*?)个', re.S)
kuaichong = re.findall(pattern_fast, str(charge[0]))
manchong = re.findall(pattern_low, str(charge[0]))
正逆地理编码——API
这一步才是最关键或者说 usefull 的东西,数据时代人们关心的是经纬度而不是一串地址,而这些充电桩的地址并不是很规范,故而先要对地址的字符串进行清洗,下述代码中的正则方法去除了地址中的全角和半角括号以及其中的内容 ,然后再调用百度的 API
# 调用 API
ak = '' # 换成你自己的 key
b = baidu.Baidu(ak)
# 去除无用信息
s = location[0].p.get_text()
delete_banjiao = re.sub(u"\\(.*?\\)|\\{.*?}|\\[.*?]", "", s)
s = bytes(delete_banjiao, encoding="utf8")
delete_quanjiao = re.sub(u"\\(.*?)|\\{.*?}|\\[.*?]|\\【.*?】", "", s.decode())
location_api = b.geocode(delete_quanjiao)
location_api.latitude
location_api.longitude
可视化
![](https://i-blog.csdnimg.cn/blog_migrate/a9dbe20b000230f060023f9cf6034dd1.jpeg)