天天看點

在django項目中導出資料到excel檔案并實作下載下傳的功能

依賴子產品

xlwt下載下傳:pip install xlwt

背景子產品

view.py

# 導出Excel檔案
def export_excel(request):
  city = request.POST.get('city')
  print(city)
  list_obj=place.objects.filter(city=city)
  # 設定HTTPResponse的類型
  response = HttpResponse(content_type='application/vnd.ms-excel')
  response['Content-Disposition'] = 'attachment;filename='+city+'.xls'
  """導出excel表"""
  if list_obj:
    # 建立工作簿
    ws = xlwt.Workbook(encoding='utf-8')
    # 添加第一頁資料表
    w = ws.add_sheet('sheet1') # 建立sheet(sheet的名稱為"sheet1")
    # 寫入表頭
    w.write(0, 0, u'地名')
    w.write(0, 1, u'次數')
    w.write(0, 2, u'經度')
    w.write(0, 3, u'緯度')
    # 寫入資料
    excel_row = 1
    for obj in list_obj:
      name = obj.place
      sum = obj.sum
      lng = obj.lng
      lat = obj.lat
      # 寫入每一行對應的資料
      w.write(excel_row, 0, name)
      w.write(excel_row, 1, sum)
      w.write(excel_row, 2, lng)
      w.write(excel_row, 3, lat)
      excel_row += 1
    # 寫出到IO
    output = BytesIO()
    ws.save(output)
    # 重新定位到開始
    output.seek(0)
    response.write(output.getvalue())
  return response           

複制

前端子產品

<button id=”export_excel” type=”button” class=”btn btn-primary col-sm-5″ style=”margin-left: 10px” 導出excel</button
$("#export_excel").click(function () {
     var csrf=$('input[name="csrfmiddlewaretoken"]').val();
     const req = new XMLHttpRequest();
     req.open('POST', '/export_excel/', true);
     req.responseType = 'blob';
     req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); //設定請求頭
     req.send('city='+$('#city').val()+"&&csrfmiddlewaretoken="+csrf); //輸入參數
     req.onload = function() {
       const data = req.response;
       const a = document.createElement('a');
       const blob = new Blob([data]);
       const blobUrl = window.URL.createObjectURL(blob);
       download(blobUrl) ;
     };

   });           

複制

function download(blobUrl) {
 var city = $("input[name='city']").val();
 const a = document.createElement('a');
 a.style.display = 'none';
 a.download = '<檔案命名 ';
 a.href = blobUrl;
 a.click();
 document.body.removeChild(a);
}           

複制

補充知識:Python Django實作MySQL百萬、千萬級的資料量下載下傳:解決memoryerror、nginx time out

前文

在用Django寫項目的時候時常需要提供檔案下載下傳的功能,而Django也是貼心提供了幾種方法:FileResponse、StreamingHttpResponse、HttpResponse,其中FileResponse和StreamingHttpResponse都是使用疊代器疊代生成資料的方法,是以适合傳輸檔案比較大的情況;而HttpResponse則是直接取得資料傳回給使用者,是以容易造成memoryerror和nginx time out(一次性取得資料和傳回的資料過多,導緻nginx逾時或者記憶體不足),關于這三者,DJango的官網也是寫的非常清楚,連接配接如下:https://docs.djangoproject.com/en/1.11/ref/request-response/

那正常我們使用的是FileResponse和StreamingHttpResponse,因為它們流式傳輸(疊代器)的特點,可以使得資料一條條的傳回給用戶端,檔案随時中斷和複傳,并且保持檔案的一緻性。

FileResponse和StreamingHttpResponse

FileResponse顧名思義,就是打開檔案然後進行傳輸,并且可以指定一次能夠傳輸的資料chunk。是以适用場景:從服務端傳回大檔案。缺點是無法實時擷取資料庫的内容并傳輸給用戶端。舉例如下:

def download(request):
 file=open('path/demo.py','rb')
  response =FileResponse(file)
  response['Content-Type']='application/octet-stream'
  response['Content-Disposition']='attachment;filename="demo.py"'
  return response           

複制

從上可以發現,檔案打開後作為參數傳入FileResponse,随後指定傳輸頭即可,但是很明顯用這個來傳輸資料庫就不太友善了,是以這邊推介用StreamingHttpResponse的方式來傳輸。

這裡就用PyMysql來取得資料,然後指定為csv的格式傳回,具體代碼如下:

# 通過pymysql取得資料
import pymysql
field_types = {
    1: 'tinyint',
    2: 'smallint',
    3: 'int'} #用于後面的字段名比對,這裡省略了大多數
conn = pymysql.connect(host='127.0.0.1',port=3306,database='demo',user='root',password='root')
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
cursor.execute(sql)
#擷取所有資料
data = cursor.fetchall()
cols = {}
#擷取所有字段
for i,row in enumerate(self.cursor.description):
 if row[0] in cols:
   cols[str(i)+row[0]] = field_types.get(row[1], str(row[1])) #這裡的field_type是類型和數字的比對
 cols[row[0]] = field_types.get(row[1], str(row[1]))
cursor.close()
conn.close()

#通過StreamingHttpResponse指定傳回格式為csv
response = StreamingHttpResponse(get_result_fromat(data, cols))
response['Content-Type'] = 'application/octet-stream'
response['Content-Disposition'] = 'attachment;filename="{0}"'.format(out_file_name)
return response

#循環所有資料,然後加到字段上傳回,注意的是要用疊代器來控制
def get_result_fromat(data, cols):
 tmp_str = ""
 # 傳回檔案的每一列列名
  for col in cols:
    tmp_str += '"%s",' % (col)
  yield tmp_str.strip(",") + "\n"
  for row in data:
    tmp_str = ""
    for col in cols:
      tmp_str += '"%s",' % (str(row[col]))
    yield tmp_str.strip(',') + "\n"           

複制

整個代碼如上,大緻分為三部分:從mysql取資料,格式化成我們想要的格式:excel、csv、txt等等,這邊指定的是csv,如果對其他格式也有興趣的可以留言,最後就是用StreamingHttpResponse指定傳回的格式傳回。

實作百萬級資料量下載下傳

上面的代碼下載下傳可以支援幾萬行甚至十幾萬行的資料,但是如果超過20萬行以上的資料,那就比較困難了,我這邊的剩餘記憶體大概是1G的樣子,當超過15萬行資料(大概)的時候,就報memoryerror了,問題就是因為fetchall,雖然我們StreamingHttpResponse是一條條的傳回,但是我們的資料時一次性批量的取得!

如何解決?以下是我的解決方法和思路:

用fetchone來代替fetchall,疊代生成fetchone

發現還是memoryerror,因為execute是一次性執行,後來發現可以用流式遊标來代替原來的普通遊标,即SSDictCursor代替DictCursor

于是整個代碼需要修改的地方如下:

cursor = conn.cursor(cursor=pymysql.cursors.DictCursor) ===

cursor = conn.cursor(cursor=pymysql.cursors.SSDictCursor)

data = cursor.fetchall() ===

row = cursor.fetchone()

def get_result_fromat(data, cols):
 tmp_str = ""
 # 傳回檔案的每一列列名
  for col in cols:
    tmp_str += '"%s",' % (col)
  yield tmp_str.strip(",") + "\n"
  for row in data:
    tmp_str = ""
    for col in cols:
      tmp_str += '"%s",' % (str(row[col]))
    yield tmp_str.strip(',') + "\n" 
    
    ===== 
    
def get_result_fromat(data, cols):
 tmp_str = ""
  for col in cols:
    tmp_str += '"%s",' % (col)
  yield tmp_str.strip(",") + "\n"
  while True:
    tmp_str = ""
    for col in cols:
      tmp_str += '"%s",' % (str(row[col]))
    yield tmp_str.strip(',') + "\n"
    row = db.cursor.fetchone()
    if row is None:
      break           

複制

可以看到就是通過while True來實作不斷地取資料下載下傳,有效避免一次性從MySQL取出記憶體不足報錯,又或者取得過久導緻nginx逾時!

總結

關于下載下傳就分享到這了,還是比較簡單的,謝謝觀看~希望能給大家一個參考。