python +selenium+Beautiful soupを使い、日本の最大級のスキルマーケットであるココナラの各サービスを取得するプログラムを考えてみました。
取得した各サービスは、CSVファイルに保存するプログラムです。
ココナラをスクレイピングするよ
簡単に私の自己紹介。
現在34歳システムエンジニア。システムエンジニア歴は、13年目。
現在は4人のチームのリーダをしています。過去に新人を3人のメンターを行なっていた経験があります。
↓udemyなら、トレンド技術から基礎技術まで幅広く学べます。返金保証もあるので、まずは気になるスキルをチェック
プログラムの概要
以下の手順でプログラムを作成していきます。
①ユーザログイン情報をsetting.jsonから読み出し
②ログイン
③各ページごとのサービス情報取得
④各サービスの詳細情報を取得
⑤取得情報を書き出し
開発環境
前回の以下の記事と構成は一緒です。
OS Windows11 chromedriver-binary 114.0.5735.16.0 selenium 4.1.0 GoogleChrome 116.0.5845.141 beautifulsoup4 4.12.2
ココナラ商品情報取得プログラム
簡単なソースコードの説明です。
①ユーザログイン情報をsetting.jsonから読み出し
以下のソースコードで、settion.jsonからユーザ情報を読み出します。
#ユーザ情報読み出し
def ReadUserInfo(file):
try :
# exeを実行している作業ディレクトリを読み込みbase_pathに格納
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
logger.debug(base_path)
with open(os.path.join(base_path, file),encoding="utf-8-sig") as f:
di = json.load(f)
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("ユーザ情報の取得に失敗しました。",e.message))
else:
logger.error("{0}".format("ユーザ情報の取得に失敗しました。"))
raise Exception("ユーザ情報の取得に失敗しました。")
return di
②ログイン
以下のソースコードでココナラでログイン動作を行います。
def Login(driver,user):
try:
driver.set_window_size(400,700)
logger.info("ログイン開始")
driver.get(user['url'])
logger.info("ログイン画面開始")
mailad = driver.find_element_by_xpath('//[@id="layout"]/div/div[1]/div[2]/header/div[1]/div[3]/nav/ul/li[4]/div[2]/a[1]')
mailad.click()
logger.info("ログイン画面遷移")
time.sleep(3)
logger.info("ログインIDを入力")
# ログインIDを入力
login_id = driver.find_element_by_id("UserLoginEmail")
login_id.send_keys(user['id'])
time.sleep(2)
logger.info("パスワードを入力")
# パスワードを入力
password = driver.find_element_by_id("UserLoginPassword")
password.send_keys(user['password'])
time.sleep(2)
logger.info("「ログイン」をクリック")
# 「ログイン」をクリック
#nextb = driver.find_element_by_id("signInSubmit")
nextb = driver.find_element_by_xpath('//[@id="mainContent"]/div/div/form/button')
nextb.click()
time.sleep(3)
logger.info("ログイン終了")
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("ログインに失敗しました。",e.message))
else:
logger.error("{0}".format("ログインに失敗しました。"))
raise Exception("ログインに失敗しました。")
return driver
③各ページごとのサービス情報取得
以下のソースコードでココナラの各サービスの取得を行います。
def RunGetSearchPageInfo(driver,user):
logger.info("各ページごとのサービス情報取得開始")
master_list=[]
try:
driver.set_window_size(400,700)
url= user['url']
page=0
while True:
list = []
#画面遷移
logger.info(url)
driver.get(url)
driver.implicitly_wait(5) # ページ読み込み最大待ち時
html = driver.page_source.encode('utf-8')
soup = BeautifulSoup(html, 'lxml')
logger.info("各ページごとのサービス情報取得:{0}ページ目開始".format(page))
results = soup.find_all("div",{"class":"c-searchPageItemList"})
#サービスURL
for info in results:
service_url = 'https://coconala.com' + info.contents[0].attrs['href']
logger.debug(service_url)
dict_en["サービスURL"] = service_url
list.append(copy.deepcopy(dict_en))
#サービス名
results = soup.find_all("p",attrs={"class":"c-serviceListItemColContentHeader_overview"})
index=0
for info in results:
list[index]["サービス名"] = info.get_text().replace("\n","").replace(" ","")
logger.debug(info.get_text().replace("\n","").replace(" ",""))
index+=1
#サムネイル画像URL
results = soup.find_all("img",attrs={"class":"c-staticImage_thumbnailImage"})
index=0
for info in results:
list[index]["サムネイル画像URL"] = info.attrs.get('src')
logger.debug(info.attrs.get('src'))
index+=1
#PR
results = soup.find_all("span",attrs={"class":"c-serviceListItemColContentHeader_servicePrLabel"})
index=0
for info in results:
list[index]["PR"] = info.get_text().replace("\n","").replace(" ","")
logger.debug(info.get_text().replace("\n","").replace(" ",""))
index+=1
#評価
results = soup.find_all("span",attrs={"class":"c-serviceListItemColContentFooterPriceRating_score"})
index=0
for info in results:
list[index]["評価"] = info.get_text().replace("\n","").replace(" ","")
logger.debug(info.get_text().replace("\n","").replace(" ",""))
index+=1
#評価数
results = soup.find_all("span",attrs={"class":"c-serviceListItemColContentFooterPriceRating_count"})
index=0
for info in results:
list[index]["評価数"] = info.get_text().replace("\n","").replace(" ","").replace("(","").replace(")","")
logger.debug(info.get_text().replace("\n","").replace(" ","").replace("(","").replace(")",""))
index+=1
#金額
results = soup.find_all("div",attrs={"class":"c-serviceListItemColContentFooterPrice_price"})
index=0
for info in results:
list[index]["金額"] = info.get_text().replace("\n","").replace(" ","")
logger.debug(info.get_text().replace("\n","").replace(" ",""))
index+=1
#取得したデータを結合する
master_list+=list
page+=1
#次ページが存在するかチェック
nextpage = soup.find("a",attrs={"class":"pagination-next"})
if nextpage!= None:
#print(nextpage)
sleep(1)
#次ページが存在しなくても、タグ上は存在するらしい。
#タグが有効かどうかでチェックする
dis = nextpage.get('disabled')
if dis == None:
url = 'https://coconala.com' + nextpage.attrs['href']
else:
#print(master_list)
break
else:
#print(master_list.len())
break
driver.implicitly_wait(2) # ページ読み込み最大待ち時
#ブラウザを閉じる
driver.quit()
logger.info("各ページごとのサービス情報取得終了")
#サービス詳細情報を取得
RunGetServiceDetail(master_list)
#csvに出力
WriteFileWithCoConara(master_list)
#utf8→SJIS変換
change_encode2()
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("想定外のエラー",e.message))
else:
logger.error("{0}".format("想定外のエラー"))
raise Exception("想定外のエラー")
少し工夫したところは、「次のページ」という表示がなくなるまでサービス情報を取得する処理です。
表示上に、次のページがなくてもタグ上に存在するらしく、タグの有効/無効を正しく判定するようにしました。
結構、こういったWebサイトがあるんですかね?
#次ページが存在するかチェック
nextpage = soup.find("a",attrs={"class":"pagination-next"})
if nextpage!= None:
#print(nextpage)
sleep(1)
#次ページが存在しなくても、タグ上は存在するらしい。
#タグが有効かどうかでチェックする
dis = nextpage.get('disabled')
if dis == None:
url = 'https://coconala.com' + nextpage.attrs['href']
else:
#print(master_list)
break
else:
#print(master_list.len())
break
④各サービスの詳細情報を取得
サービス一覧の情報を取得したら、最後に各サービスの詳細情報を取得します。
以下のソースコードでココナラの各サービスの詳細情報取得を行います。
def RunGetServiceDetail(data_list):
logger.info("サービス詳細情報取得開始")
try:
t = 0
for data in data_list:
logger.info("サービス詳細情報取得:{0}行目開始".format(t))
service_url = data["サービスURL"]
ans = urreq.urlopen(service_url)
soup = BeautifulSoup(ans, "html.parser")
#出品者名
ans_word = soup.find("div",attrs={"class":"c-providerInfo_name c-providerName"})
logger.debug(ans_word.get_text())
data_list[t]["出品者名"] = ans_word.get_text().replace("\n","").replace(" ","")
#出品者URL
profile_link = soup.find("a",attrs={"class":"c-serviceDetailProvider_link"})
url = 'https://coconala.com' + profile_link.attrs.get('href')
logger.debug(url)
data_list[t]["出品者URL"] = url
logger.info("サービス詳細情報取得:{0}行目終了".format(t))
t+=1
sleep(1)
logger.info("サービス詳細情報取得終了")
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("想定外のエラー",e.message))
else:
logger.error("{0}".format("想定外のエラー"))
raise Exception("想定外のエラー")
⑤取得情報を書き出し、実行時間を書き出し。
一度UTF-8でCSV出力して、UTF-8のファイルをSJISに変換しています。
def WriteFileWithCoConara(data):
logger.info("csvファイル書き込み開始")
try:
# CSVファイルを書き込みモードで開く
parent = Path(__file__).resolve().parent
# exeを実行している作業ディレクトリを読み込みbase_pathに格納
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
#with open(os.path.join(base_path, "CoConaraData_tmp.csv"), encoding='cp932',mode='w', newline='') as file:
with open(os.path.join(base_path, date_str+utf8_file_name),encoding='utf8', mode='w', newline='') as file:
#print(data)
csv_writer = csv.writer(file)
csv_writer = csv.DictWriter(file, fieldnames=dict_en)
csv_writer.writeheader()
# データをCSVファイルに書き込む
csv_writer.writerows(data)
logger.info("csvファイル書き込み終了")
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("CSVファイルの書き込みに失敗しました。",e.message))
else:
logger.error("{0}".format("CSVファイルの書き込みに失敗しました。"))
raise Exception("CSVファイルの書き込みに失敗しました。")
def change_encode2():
logger.info("utf8→SJIS変換開始")
try:
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
with open(os.path.join(base_path, date_str+utf8_file_name),encoding='utf8', errors='replace') as fin:
with open(os.path.join(base_path,date_str+sjis_file_name), 'w', encoding='cp932',errors='replace') as fout:
fout.write(fin.read())
logger.info("tf8→SJIS変換終了")
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("エンコードに失敗しました。",e.message))
else:
logger.error("{0}".format("エンコードに失敗しました。"))
raise Exception("エンコードに失敗しました。")
上記の各機能が一つになったソースコードです。
ファイル名:CoConaraInfo.py
import os
import sys
import json
import csv
import pprint
from pathlib import Path
import time
import copy
import requests
from time import sleep
import codecs
import io
# WebDriverライブラリをインポート
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
# ChromeのWebDriverライブラリをインポート
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from bs4 import BeautifulSoup
import re
import urllib.request as urreq
import logging
import datetime
#ユーザ/パスワード
username = ""
mypassword = ""
dict_en = {'PR':"",'サービスURL':"",'サービス名':"",'評価':"",'評価数':"",'金額':"",'サムネイル画像URL':"",'出品者名':"",'出品者URL':""}
utf8_file_name = "_CoConaraData_utf8.csv"
sjis_file_name = "_CoConaraData_sjis.csv"
LogFileName='CoconaraGetInfo.log'
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
if True==os.path.isfile(os.path.join(base_path, LogFileName)):
os.remove(os.path.join(base_path, LogFileName))
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s:%(name)s - %(message)s')
file_handler = logging.FileHandler(LogFileName)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
RuntimeFile="_runtime.txt"
dt_now=None
date_str=""
class Mylogger:
_instance = None
logger=None
@staticmethod
def getInstance():
if Mylogger._instance == None:
Mylogger()
return Mylogger._instance
def init(self):
if Mylogger._instance != None:
raise Exception("This is not Singleton !!")
else:
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
if True==os.path.isfile(os.path.join(base_path, LogFileName)):
os.remove(os.path.join(base_path, LogFileName))
self.logger = logging.getLogger(name)
self.logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s:%(name)s - %(message)s')
file_handler = logging.FileHandler(LogFileName)
file_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
Mylogger._instance = self
def setParam(self, p):
self.p = p
pass
def getParam(self):
return self.p
#ユーザ情報読み出し
def ReadUserInfo(file):
try :
# exeを実行している作業ディレクトリを読み込みbase_pathに格納
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
logger.debug(base_path)
with open(os.path.join(base_path, file),encoding="utf-8-sig") as f:
di = json.load(f)
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("ユーザ情報の取得に失敗しました。",e.message))
else:
logger.error("{0}".format("ユーザ情報の取得に失敗しました。"))
raise Exception("ユーザ情報の取得に失敗しました。")
return di
def WriteFileWithCoConara(data):
logger.info("csvファイル書き込み開始")
try:
# CSVファイルを書き込みモードで開く
parent = Path(file).resolve().parent
# exeを実行している作業ディレクトリを読み込みbase_pathに格納
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
#with open(os.path.join(base_path, "CoConaraData_tmp.csv"), encoding='cp932',mode='w', newline='') as file:
with open(os.path.join(base_path, date_str+utf8_file_name),encoding='utf8', mode='w', newline='') as file:
#print(data)
csv_writer = csv.writer(file)
csv_writer = csv.DictWriter(file, fieldnames=dict_en)
csv_writer.writeheader()
# データをCSVファイルに書き込む
csv_writer.writerows(data)
logger.info("csvファイル書き込み終了")
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("CSVファイルの書き込みに失敗しました。",e.message))
else:
logger.error("{0}".format("CSVファイルの書き込みに失敗しました。"))
raise Exception("CSVファイルの書き込みに失敗しました。")
def change_encode2():
logger.info("utf8→SJIS変換開始")
try:
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
with open(os.path.join(base_path, date_str+utf8_file_name),encoding='utf8', errors='replace') as fin:
with open(os.path.join(base_path,date_str+sjis_file_name), 'w', encoding='cp932',errors='replace') as fout:
fout.write(fin.read())
logger.info("tf8→SJIS変換終了")
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("エンコードに失敗しました。",e.message))
else:
logger.error("{0}".format("エンコードに失敗しました。"))
raise Exception("エンコードに失敗しました。")
def Login(driver,user):
try:
driver.set_window_size(400,700)
logger.info("ログイン開始")
driver.get(user['url'])
logger.info("ログイン画面開始")
mailad = driver.find_element_by_xpath('//[@id="layout"]/div/div[1]/div[2]/header/div[1]/div[3]/nav/ul/li[4]/div[2]/a[1]')
mailad.click()
logger.info("ログイン画面遷移")
time.sleep(3)
logger.info("ログインIDを入力")
# ログインIDを入力
login_id = driver.find_element_by_id("UserLoginEmail")
login_id.send_keys(user['id'])
time.sleep(2)
logger.info("パスワードを入力")
# パスワードを入力
password = driver.find_element_by_id("UserLoginPassword")
password.send_keys(user['password'])
time.sleep(2)
logger.info("「ログイン」をクリック")
# 「ログイン」をクリック
#nextb = driver.find_element_by_id("signInSubmit")
nextb = driver.find_element_by_xpath('//[@id="mainContent"]/div/div/form/button')
nextb.click()
time.sleep(3)
logger.info("ログイン終了")
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("ログインに失敗しました。",e.message))
else:
logger.error("{0}".format("ログインに失敗しました。"))
raise Exception("ログインに失敗しました。")
return driver
def RunGetServiceDetail(data_list):
logger.info("サービス詳細情報取得開始")
try:
t = 0
for data in data_list:
logger.info("サービス詳細情報取得:{0}行目開始".format(t))
service_url = data["サービスURL"]
ans = urreq.urlopen(service_url)
soup = BeautifulSoup(ans, "html.parser")
#出品者名
ans_word = soup.find("div",attrs={"class":"c-providerInfo_name c-providerName"})
logger.debug(ans_word.get_text())
data_list[t]["出品者名"] = ans_word.get_text().replace("\n","").replace(" ","")
#出品者URL
profile_link = soup.find("a",attrs={"class":"c-serviceDetailProvider_link"})
url = 'https://coconala.com' + profile_link.attrs.get('href')
logger.debug(url)
data_list[t]["出品者URL"] = url
logger.info("サービス詳細情報取得:{0}行目終了".format(t))
t+=1
sleep(1)
logger.info("サービス詳細情報取得終了")
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("想定外のエラー",e.message))
else:
logger.error("{0}".format("想定外のエラー"))
raise Exception("想定外のエラー")
def RunGetSearchPageInfo(driver,user):
logger.info("各ページごとのサービス情報取得開始")
master_list=[]
try:
driver.set_window_size(400,700)
url= user['url']
page=0
while True:
list = []
#画面遷移
logger.info(url)
driver.get(url)
driver.implicitly_wait(5) # ページ読み込み最大待ち時
html = driver.page_source.encode('utf-8')
soup = BeautifulSoup(html, 'lxml')
logger.info("各ページごとのサービス情報取得:{0}ページ目開始".format(page))
results = soup.find_all("div",{"class":"c-searchPageItemList"})
#サービスURL
for info in results:
service_url = 'https://coconala.com' + info.contents[0].attrs['href']
logger.debug(service_url)
dict_en["サービスURL"] = service_url
list.append(copy.deepcopy(dict_en))
#サービス名
results = soup.find_all("p",attrs={"class":"c-serviceListItemColContentHeader_overview"})
index=0
for info in results:
list[index]["サービス名"] = info.get_text().replace("\n","").replace(" ","")
logger.debug(info.get_text().replace("\n","").replace(" ",""))
index+=1
#サムネイル画像URL
results = soup.find_all("img",attrs={"class":"c-staticImage_thumbnailImage"})
index=0
for info in results:
list[index]["サムネイル画像URL"] = info.attrs.get('src')
logger.debug(info.attrs.get('src'))
index+=1
#PR
results = soup.find_all("span",attrs={"class":"c-serviceListItemColContentHeader_servicePrLabel"})
index=0
for info in results:
list[index]["PR"] = info.get_text().replace("\n","").replace(" ","")
logger.debug(info.get_text().replace("\n","").replace(" ",""))
index+=1
#評価
results = soup.find_all("span",attrs={"class":"c-serviceListItemColContentFooterPriceRating_score"})
index=0
for info in results:
list[index]["評価"] = info.get_text().replace("\n","").replace(" ","")
logger.debug(info.get_text().replace("\n","").replace(" ",""))
index+=1
#評価数
results = soup.find_all("span",attrs={"class":"c-serviceListItemColContentFooterPriceRating_count"})
index=0
for info in results:
list[index]["評価数"] = info.get_text().replace("\n","").replace(" ","").replace("(","").replace(")","")
logger.debug(info.get_text().replace("\n","").replace(" ","").replace("(","").replace(")",""))
index+=1
#金額
results = soup.find_all("div",attrs={"class":"c-serviceListItemColContentFooterPrice_price"})
index=0
for info in results:
list[index]["金額"] = info.get_text().replace("\n","").replace(" ","")
logger.debug(info.get_text().replace("\n","").replace(" ",""))
index+=1
#取得したデータを結合する
master_list+=list
page+=1
#次ページが存在するかチェック
nextpage = soup.find("a",attrs={"class":"pagination-next"})
if nextpage!= None:
#print(nextpage)
sleep(1)
#次ページが存在しなくても、タグ上は存在するらしい。
#タグが有効かどうかでチェックする
dis = nextpage.get('disabled')
if dis == None:
url = 'https://coconala.com' + nextpage.attrs['href']
else:
#print(master_list)
break
else:
#print(master_list.len())
break
driver.implicitly_wait(2) # ページ読み込み最大待ち時
#ブラウザを閉じる
driver.quit()
logger.info("各ページごとのサービス情報取得終了")
#サービス詳細情報を取得
RunGetServiceDetail(master_list)
#csvに出力
WriteFileWithCoConara(master_list)
#utf8→SJIS変換
change_encode2()
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("想定外のエラー",e.message))
else:
logger.error("{0}".format("想定外のエラー"))
raise Exception("想定外のエラー")
#エラーダイアログ表示
def ExitDialog(message):
#er = tkinter.Tk()
#er.withdraw()
#messagebox.showinfo('メッセージ', message)
#er.destroy()
logger.error("{0}".format("アプリケーションを終了"))
sys.exit(1)
def WriteRunTime(message):
base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
with open(os.path.join(base_path, date_str+RuntimeFile), 'w') as f:
f.write(message)
def elapsed_time_str(seconds):
"""秒をhh:mm:ss形式の文字列で返す
Parameters
----------
seconds : float
表示する秒数
Returns
-------
str
hh:mm:ss形式の文字列
"""
seconds = int(seconds + 0.5) # 秒数を四捨五入
h = seconds // 3600 # 時の取得
m = (seconds - h 3600) // 60 # 分の取得
s = seconds - h 3600 - m 60 # 秒の取得
return f"{h:02}:{m:02}:{s:02}" # hh:mm:ss形式の文字列で返す
if name == 'main__':
print("収集開始")
options = webdriver.ChromeOptions()
options.add_argument("--headless")
start = time.time()
#ログイン情報取得
try:
driver = webdriver.Chrome(options=options,service=ChromeService(ChromeDriverManager().install()))
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("Webドライバの起動に失敗しました。",e.message))
else:
logger.error("{0}".format("Webドライバの起動に失敗しました。"))
raise Exception("Webドライバの起動に失敗しました。")
#ウインドサイズの指定
driver.set_window_size(400,700)
driver.implicitly_wait(3) # ページ読み込み最大待ち時
dt_now = datetime.datetime.now()
date_str=dtnow.strftime('%Y%m%d%H%M%S')
try:
#setting.json読み込み
di=ReadUserInfo("setting.json")
#ログイン
Login(driver,di)
RunGetSearchPageInfo(driver,di)
print("収集終了")
end = time.time() # 現在時刻(処理完了後)を取得
time_diff = end - start # 処理完了後の時刻から処理開始前の時刻を減算する
logger.info("実行時間:{0}".format(elapsed_time_str(time_diff))) # 処理にかかった時間データを使用
WriteRunTime("実行時間:{0}".format(elapsed_time_str(time_diff)))
except Exception as e:
if hasattr(e, 'message'):
logger.error("{0}{1}".format("想定外のエラー",e.message))
else:
logger.error("{0}".format("想定外のエラー"))
raise Exception("想定外のエラー")
ココナラでのユーザアカウントの設定ファイルは以下のように設定してください。
settiong.json
{
"id":"×××××××",
"password":"××××××",
"url": "https://coconala.com/categories/687"//参考URLです。
}
まとめ
ココナラのURLを設定すればそのまま使えると思います。かなり便利なはず。
作成段階で、Googleスプレッドシートにそのまま出力しようと思いましたが、Google Drive APIを使用する必要がありめんどくさいの断念しました。
そこまで代替案として、もっともお手軽なCSVに出力する形になりました。
どうしても、スプレッドシートを使いたい人は、Google Cloud Platformの利用登録が必要となります。
Google Driveにスプレッドシートを使わず、Googleドライブにファイルを共有するやり方は今度記事にしたいと思います。
↓udemyなら、トレンド技術から基礎技術まで幅広く学べます。返金保証もあるので、まずは気になるスキルをチェック
コメント