python 對城市距離自動化爬取-小型專案

娃哈哈店長發表於2019-12-26

描述:
本地建立資料庫,將excel資料儲存到city表中,再取|湖北省|的所有地級市和縣、縣級市、區資料作為樣表資料記錄在樣表中。利用python的xlrd包,定義process_data包來存放操作excel資料,生成sql語句的類,定義op_postgresql包來存放資料庫的操作物件,定義各種方法

本地建立資料庫,將excel資料儲存到city表中,再取|湖北省|的所有地級市和縣、縣級市、區資料作為樣表資料記錄在樣表中。

部落格地址:https://boywithacoin.cn/
專案的完整地址在https://github.com/Freen247/python_get_cit...
有興趣的可以給我評論和star/issue哦?~ (ง •_•)ง

本地建立資料庫,將excel資料儲存到city表中,再取|湖北省|的所有地級市和縣、縣級市、區資料作為樣表資料記錄在樣表中。準備工作建立好public/config.py擴充套件包,到時候,利用python的xlrd包,定義process_data包來存放操作excel資料,生成sql語句的類,
定義op_postgresql包來存放資料庫的操作物件,定義各種方法
建立crwler包,來存放爬蟲的操作物件 -> 發現對方網站呼叫的地圖api -> 更改為呼叫德地圖api的包-存放操作物件
建立log資料夾,存放資料庫操作的日誌
建立data資料夾,存放初始excel資料

資料庫基本構造:

樣本資料表格式:
表名:sample_table

name column data type length 分佈 fk 必填域 備註
地域名 address text TRUE 地域名
地域型別 ad_type integer TRUE 0-為地級市;1-為縣、縣級市、區。
經緯度 coordinates text TRUE 地域名的經緯度
···

樣本1-1地點route表的格式

表名:sample_route

name column data type length 分佈 fk 必填域 備註
出發點 origin text 出發點
目的點 destination text 目的點
距離 distance integer 距離
路線 route text 路線
···

建立配置資訊介面

方便儲存我們需要的特定變數和配置資訊。

public/config.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
#__author__: stray_camel

import os,sys
#當前package所在目錄的上級目錄
src_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

建立讀取excel資料的介面

利用python的xlrd包,定義process_data包來存放操作excel資料,生成sql語句的類

參考github原始碼readme文件
並沒有發現在PyPI上有document,所以只能去github上找原始碼了,xlrd處理excel基礎guide

import xlrd
book = xlrd.open_workbook("myfile.xls")
print("The number of worksheets is {0}".format(book.nsheets))
print("Worksheet name(s): {0}".format(book.sheet_names()))
sh = book.sheet_by_index(0)
print("{0} {1} {2}".format(sh.name, sh.nrows, sh.ncols))
print("Cell D30 is {0}".format(sh.cell_value(rowx=29, colx=3)))
for rx in range(sh.nrows):
print(sh.row(rx))

建立process_data/excel2sql.py擴充套件包,方便後面import
獲取excel的資料構造sql語句,建立city表(湖北省)樣表

process_data/excel2sql.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
#__author__: stray_camel

import xlrd,sys,os,logging
from public import config
class Excel2Sql(object):
    def __init__(
        self, 
        url:"str型別的檔案路徑", 
        sheet:"excel中的表單名"):
        self.f_name = url
        # 將excel中特定表單名資料儲存起來
        self.sh_data = xlrd.open_workbook(self.f_name).sheet_by_name(sheet)
        self.rows = self.sh_data.nrows
        self.cols = self.sh_data.ncols

當我們生成這個Excel2Sql物件的時候,我們希望按照類似

excel_data = excel2sql.Excel2Sql("fiel_name","sheet_name")

的程式碼形式來直接讀取excel檔案並獲取某個表單的資料。所以在初始化物件的時候我們希望對其屬性進行賦值。

excel表中,我們按照下面的形式進行儲存資料:

省/直轄市 地級市 縣、縣級市、區
北京市 北京市 東城區
... ... ...

之後我們希望透過呼叫這個類(介面)地時候能夠訪問其中一個函式,只獲取某個省/或者直轄市的所有資料,類似湖北省,我們指向獲取奇中103個縣、區。

在類Excel2Sql中定義方法:

def init_SampleViaProvince_name(
        self, 
        Province_name:"省名"
        ) ->"insert的資料,列表形式[('地域名1','1','經緯度'),('地域名2','1','經緯度')]":
        geo_app = Geo_mapInterface(config.geo_key)
        all_data = [self.sh_data.row_values(i) for i in range(self.rows)]

        cities_data=[[["".join(i),1],["".join(i[1:len(i)]),1]][i[0]==i[1]] for i in all_data if i[0] == Province_name]
        for i in cities_data:
            i.append(geo_app.get_coordinatesViaaddress("".join(i[0])))
        # cities_data=[[["".join(i),1,'test1'],["".join(i[1:len(i)]),1,'test2']][i[0]==i[1]] for i in all_data if i[0] == Province_name]
        return cities_data

之後我們可以測試類的構造是否正確,或進行除錯:
在檔案末端編寫:

if __name__ == "__main__":
    test = Excel2Sql(config.src_path+"\\data\\2019最新全國城市省市縣區行政級別對照表(194).xls","全國城市省市縣區域列表")
    print(test.init_SampleViaProvince_name("北京市"))

測試結果:

(env) PS F:\覽眾資料> & f:/覽眾資料/env/Scripts/python.exe f:/覽眾資料/城市距離爬取/process_data/excel2sql.py
[['北京市東城區', 1, '116.416357,39.928353'], ['北京市西城區', 1, '116.365868,39.912289'], ['北京市崇文區', 1,
'116.416357,39.928353'], ['北京市宣武區', 1, '116.365868,39.912289'], ['北京市朝陽區', 1, '116.601144,39.948574'], ['北京市豐臺區', 1, '116.287149,39.858427'], ['北京市石景山區', 1, '116.222982,39.906611'], ['北京市海淀區', 1, '116.329519,39.972134'], ['北京市門頭溝區', 1, '116.102009,39.940646'], ['北京市房山區', 1, '116.143267,39.749144'], ['北京市通州區', 1, '116.656435,39.909946'], ['北京市順義區', 1, '116.654561,40.130347'], ['北京市昌
平區', 1, '116.231204,40.220660'], ['北京市大興區', 1, '116.341014,39.784747'], ['北京市平谷區', 1, '117.121383,40.140701'], ['北京市懷柔區', 1, '116.642349,40.315704'], ['北京市密雲縣', 1, '116.843177,40.376834'], ['北京
市延慶縣', 1, '115.974848,40.456951']]

建立OP資料庫postgresql(其他資料庫也都一樣啦~)介面

定義op_postgresql包來存放資料庫的操作物件,定義各種方法

資料庫的curd真的是從大二寫到大四。
訪問postgresql資料庫一般用的包:psycopg2
訪問官網
在這個操作文件網站中,使用的思路已經很清楚的寫出來了http://initd.org/psycopg/docs/usage.html

希望大小少在網上走彎路(少看一些翻譯過來的文件)。。。
http://initd.org/psycopg/

模式還是一樣,呼叫postgresql的驅動/介面,設定引數登陸,訪問資料庫。設定游標,注入sql資料,fetch返回值。

  • 這裡需要注意的幾點是,預設防xss注入,寫程式碼時一般設定引數訪問。
  • 注意生成日誌檔案,列印日誌

具體過程不贅述,直接上程式碼

op_postgresql/opsql.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-
#__author__: stray_camel

'''
定義對mysql資料庫基本操作的封裝
1.資料插入
2.表的清空
3.查詢表的所有資料
'''
import logging
import psycopg2
from public import config
class OperationDbInterface(object):
    #定義初始化連線資料庫
    def __init__(self, 
    host_db : '資料庫服務主機' = 'localhost', 
    user_db: '資料庫使用者名稱' = 'postgres', 
    passwd_db: '資料庫密碼' = '1026shenyang', 
    name_db: '資料庫名稱' = 'linezone', 
    port_db: '埠號,整型數字'=5432):
        try:
            self.conn=psycopg2.connect(database=name_db, user=user_db, password=passwd_db, host=host_db, port=port_db)#建立資料庫連結
        except psycopg2.Error as e:
            print("建立資料庫連線失敗|postgresql Error %d: %s" % (e.args[0], e.args[1]))
            logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG,format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
            logger = logging.getLogger(__name__)
            logger.exception(e)
        self.cur=self.conn.cursor()

    #定義在樣本表中插入資料操作
    def insert_sample_data(self, 
    condition : "insert語句" = "insert into sample_data(address,ad_type,coordinates) values (%s,%s,%s)", 
    params : "insert資料,列表形式[('地域名1','1','經緯度'),('地域名2','1','經緯度')]" = [('地域名1','1','經緯度'),('地域名2','1','經緯度')]
    ) -> "字典形式的批次插入資料結果" :
        try:
            self.cur.executemany(condition,params)
            self.conn.commit()
            result={'code':'0000','message':'執行批次插入操作成功','data':len(params)}
            logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
            logger = logging.getLogger(__name__)
            logger.info("在樣本表sample_data中插入資料{}條,操作:{}!".format(result['data'],result['message']))
        except psycopg2.Error as e:
            self.conn.rollback()  # 執行回滾操作
            result={'code':'9999','message':'執行批次插入異常','data':[]}
            print ("資料庫錯誤|insert_data : %s" % (e.args[0]))
            logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
            logger = logging.getLogger(__name__)
            logger.exception(e)
        return result

繼續寫(程式碼長了,怕顯示出錯)

    #定義在sample_route表中插入資料操作
    def insert_sample_route(self, 
    condition : "insert語句" ,
    params : "insert語句的值"
    )->"字典形式的批次插入資料結果":
        try:
            self.cur.executemany(condition,params)
            self.conn.commit()
            result={'code':'0000','message':'執行批次插入操作成功','data':len(params)}
            logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
            logger = logging.getLogger(__name__)
            logger.info("在樣本表sample_route中插入資料{}條,操作:{}!".format(result['data'],result['message']))
        except psycopg2.Error as e:
            self.conn.rollback()  # 執行回滾操作
            result={'code':'9999','message':'執行批次插入異常','data':[]}
            print ("資料庫錯誤|insert_data : %s" % (e.args[0]))
            logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
            logger = logging.getLogger(__name__)
            logger.exception(e)
        return result

    #定義對錶的清空
    def ini_table(self,
    tablename:"表名")->"清空表資料結果":
        try:
            rows_affect = self.cur.execute("select count(*) from {}".format(tablename))
            test = self.cur.fetchone()  # 獲取一條結果
            self.cur.execute("truncate table {}".format(tablename))
            self.conn.commit()
            result={'code':'0000','message':'執行清空表操作成功','data':test[0]}
            logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
            logger = logging.getLogger(__name__)
            logger.info("清空{}表,運算元據{}條,操作:{}!".format(tablename,result['data'],result['message']))
        except psycopg2.Error as e:
            self.conn.rollback()  # 執行回滾操作
            result={'code':'9999','message':'執行批次插入異常','data':[]}
            print ("資料庫錯誤|insert_data : %s" % (e.args[0]))
            logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
            logger = logging.getLogger(__name__)
            logger.exception(e)
        return result

    #查詢表的所有資料
    def select_all(self, 
    tablename:"表名")->"返回list,存放查詢的結果":
        try:
            rows_affect = self.cur.execute("select * from {}".format(tablename))
            test = self.cur.fetchall()  
            # self.cur.execute("truncate table {}".format(tablename))
            self.conn.commit()
            result={'code':'0000','message':'查詢表成功','data':test}
            logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
            logger = logging.getLogger(__name__)
            logger.info("清空{}表,運算元據{}條,操作:{}!".format(tablename,result['data'],result['message']))
        except psycopg2.Error as e:
            self.conn.rollback()  # 執行回滾操作
            result={'code':'9999','message':'執行批次插入異常','data':[]}
            print ("資料庫錯誤|insert_data : %s" % (e.args[0]))
            logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
            logger = logging.getLogger(__name__)
            logger.exception(e)
        return result
    #資料庫關閉
    def __del__(self):
        self.conn.close()

這裡提出來想說一下的列印日誌檔案的操作,:
參考檔案:
https://docs.python.org/zh-cn/3/library/lo...
https://docs.python.org/zh-cn/3/library/lo...
logging作為python老牌庫,在https://docs.python.org/zh-cn/3/library/in...中一般都搜尋的到,引數的說明不過多的贅述。
因為我的程式碼都是用utf-8寫的所以在basicConfig配置時,加入了utf-8的資訊。

result={'code':'9999','message':'執行批次插入異常','data':[]}
            print ("資料庫錯誤|insert_data : %s" % (e.args[0]))
            logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
            logger = logging.getLogger(__name__)
            logger.exception(e)

測試爬取https://licheng.supfree.net/網站

測試https://licheng.supfree.net/網站是否可以傳參進行post,獲取request後的兩地的地理距離

  • 測試網站是否有反爬蟲機制,結果無。

透過測試request,設定測試地點洪山區和江夏區,網站顯示距離為16.5公里
解析html發現
測試結果:網站的資料是透過js檔案獲取傳參的。

var map = new BMap.Map("container");
map.centerAndZoom(new BMap.Point(116.404, 39.915), 14);

var oGl = document.getElementById("div_gongli");
var ofname = document.getElementById("tbxArea");
var otname = document.getElementById("tbxAreaTo");
if (ofname.value != "" && otname.value != "") {
    var output = "全程:";
    var searchComplete = function(results) {
        if (transit.getStatus() != BMAP_STATUS_SUCCESS) {
            return;
        }
        var plan = results.getPlan(0);
        output += plan.getDistance(true); //獲取距離
        output += "/";
        output += plan.getDuration(true); //獲取時間
    }
    var transit = new BMap.DrivingRoute(map, {
        renderOptions: {
            map: map,
            panel: "results",
            autoViewport: true
        },
        onSearchComplete: searchComplete,
        onPolylinesSet: function() {
            oGl.innerText = output;
        }
    });
    transit.search(ofname.value, otname.value);
}
...

我們檢視網站載入的js檔案,發現獲取Bmap這個物件原來是來自於

https://api.map.baidu.com/?qt=nav&c=131&sn=2%24%24%24%24%24%24%E6%B4%AA%E5%B1%B1%E5%8C%BA%24%240%24%24%24%24&en=2%24%24%24%24%24%24%E6%B1%9F%E5%A4%8F%E5%8C%BA%24%240%24%24%24%24&sy=0&ie=utf-8&oue=1&fromproduct=jsapi&res=api&callback=BMap._rd._cbk35162&ak=zS6eHWhoEwXMUrQKkaaTlvY65XsVykFf

很明顯,這個網站也是呼叫的百度的api。
我們檢視js檔案傳遞的部分引數:

content: {dis: 16538,…}
dis: 16538
kps: [{a: 7, dr: "", dw: 0, ett: 17, ic: "", iw: 0, pt: ".=zl83LBgOCJVA;", rt: 1, tt: 1},…]
rss: [{d: 0, g: "", n: "", rr: 0, t: 0, tr: 0},…]
taxi: {detail: [{desc: "白天(05:00-23:00)", kmPrice: "2.3", startPrice: "14.0", totalPrice: "47"},…],…}
time: 1516
toll: 0
...

核實content裡的dis和time是否就是網站顯示的距離和時間
當我們更換測試地點後,顯示的距離和https://api.map.baidu.com 中content的內容一樣
time:1516%60=25.26666666666667‬,和顯示的26分鐘也是核對的。

測試結果:網站沒有反爬蟲機制,但是呼叫的是百度地圖pai獲取數。

  • 網站儲存地址的資料是按照編碼來的,對應的下級城市為小數
    比如熱門城市:

    hot_city: ["北京市|131", "上海市|289", "廣州市|257", "深圳市|340", "成都市|75", "天津市|332", "南京市|315", "杭州市|179", "武漢市|218",…]
    0: "北京市|131"
    1: "上海市|289"
    2: "廣州市|257"
    3: "深圳市|340"
    4: "成都市|75"
    5: "天津市|332"
    6: "南京市|315"
    7: "杭州市|179"
    8: "武漢市|218"
    9: "重慶市|132"

    當測試區級地點:(洪山區、江夏區)

    map.centerAndZoom(new BMap.Point(116.404, 39.915), 14);
  • 如果不行能否呼叫高德地圖api?

建立介面-呼叫高德地圖api

在高德的管理平臺註冊個人開發:https://lbs.amap.com/dev/key/app

申請個人的key。每日呼叫量有上線,所以只能一點點的做。
我們將申請到的key寫入配置資訊檔案中:
public/config.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
#__author__: stray_camel

import os,sys
#當前package所在目錄的上級目錄
src_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
geo_key = '3e2235273ddtestdef4'
#key我已經打馬賽克了,請自己去申請

完成功能:
透過地域名查詢經緯度;
對出發/目的地點-路程-路線,資料進行查詢,並插入到資料庫中,現已實現。但對於資料量較多的情況,資料庫的操作較慢。

首先前往高德地圖註冊個人使用者,獲取一個key,之後我們可以透過構造url,透過request來獲取資料。

透過address獲取經緯度:

def get_coordinatesViaaddress(self, 
    address:"地點名"
    ) -> "返回str型別的經緯度":
        url='https://restapi.amap.com/v3/geocode/geo?address='+address+'&output=json&key='+self.key
        #將一些符號進行URL編碼
        newUrl = parse.quote(url, safe="/:=&?#+!$,;'@()*
")
        coor = json.loads(urllib.request.urlopen(newUrl).read())['geocodes'][0]['location']
        logging.basicConfig(stream=open(config.src_path + '/log/syserror.log', encoding="utf-8", mode="a"), level = logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
        logger = logging.getLogger(__name__)
        logger.info("查詢{}的經緯度:{}!".format(address,coor))
        # print()
        return coor

透過城市list獲取兩點之間距離和出行方式:

def get_disViaCoordinates(self,
    addList:"一個列表存放地址資料"
    ) ->  "{'origin':[],'destination':[],'distance':[],'route':[]}":
        dict_route = {'origin':[],'destination':[],'distance':[],'route':[]}
        for m in range(len(addList)):    
            for n in range(m,len(addList)):
                if m!=n:
                    print('get_tetst',m,n)
                    #從addList中得到地址的名稱,經緯度
                    origin = addList[m][2]
                    destination = addList[n][2]
                    url2='https://restapi.amap.com/v3/direction/driving?origin='+origin+'&destination='+destination+'&extensions=all&output=json&key=3e2235273dd2c0ca2421071fbb96def4'
                #編碼
                    newUrl2 = parse.quote(url2, safe="/:=&?#+!$,;'@()*[]")
                    #傳送請求
                    response2 = urllib.request.urlopen(newUrl2)
                    #接收資料
                    data2 = response2.read()
                    #解析json檔案
                    jsonData2 = json.loads(data2)
                    #輸出該json中所有road的值
                    # print(jsonData2)
                    road=jsonpath.jsonpath(jsonData2,'$..road')
                    #從json檔案中提取距離
                    distance = jsonData2['route']['paths'][0]['distance']
                    #字典dict_route中追加資料
                    dict_route.setdefault("origin",[]).append(addList[m][0])
                    dict_route.setdefault("destination",[]).append(addList[n][0])
                    dict_route.setdefault("distance",[]).append(distance)
                    dict_route.setdefault("route",[]).append(road)
        return dict_route

資料庫樣品:
sample_table

資料庫的內容我就用json表示了哈:

[
  {
    "address": "湖北省武漢市江岸區",
    "ad_type": 1,
    "coordinates": "114.278760,30.592688"
  },
  {
    "address": "湖北省武漢市江漢區",
    "ad_type": 1,
    "coordinates": "114.270871,30.601430"
  },
  {
    "address": "湖北省武漢市喬口區",
    "ad_type": 1,
    "coordinates": "114.214920,30.582202"
  },
  ...共103條地點資料

sample_route,以sample_table前三個資料為例做出查詢,和返回。

[
  {
    "origin": "湖北省武漢市江岸區",
    "destination": "湖北省武漢市江漢區",
    "route": "['臺北一路', '新華路']",
    "distance": "1520"
  },
  {
    "origin": "湖北省武漢市江岸區",
    "destination": "湖北省武漢市喬口區",
    "route": "['臺北一路', '臺北路', '解放大道', '解放大道', '解放大道', '解放大道', '解放大道', '解放大道', '解放大道', '解放大道', '二環線輔路', '沿河大道']",
    "distance": "9197"
  },
  {
    "origin": "湖北省武漢市江漢區",
    "destination": "湖北省武漢市喬口區",
    "route": "['新華路', '建設大道', '建設大道', '建設大道', '建設大道', '沿河大道']",
    "distance": "7428"
  }
]

BUG:
問題:在process_data/excel2sql.py,呼叫格比public/config.py介面
問題:當我們訪問隔壁資料夾的介面時,如果發現呼叫不了,可以在當前檔案的頭部加入:

import sys,os
absPath = os.path.abspath(__file__)   #返回程式碼段所在的位置,肯定是在某個.py檔案中
temPath = os.path.dirname(absPath)    #往上返回一級目錄,得到檔案所在的路徑
temPath = os.path.dirname(temPath)    #在往上返回一級,得到資料夾所在的路徑
sys.path.append(temPath)

將當前資料夾所在的路徑加入到python系統路徑中

本作品採用《CC 協議》,轉載必須註明作者和本文連結
文章!!首發於我的部落格Stray_Camel(^U^)ノ~YO

相關文章