twitterのスクレイピングbot
Tweetホーム > 個人開発したことまとめ > twitterのスクレイピングbot
twitterのスクレイピングbot
ロマサガRSというソシャゲをやってて、新キャラが実装されるたびに絵師さんがこんなツイートしてたりする。
キャラクタは大量にいるし、いつ誰が公開するかもわからないので自動的に情報収集して検索できるようなもの作れないかな?【仕事絵】
— 木野田 永志 (@bittercolors) June 16, 2022
SQUARE ENIX様の「ロマンシング サガ リ・ユニバース」にて、
『SSグレイ』を描かせて頂きました。https://t.co/YoMfUvbRR8#新ロマサガRS#ロマサガRS pic.twitter.com/Z3BCV7bphQ
⇒そうだ!スクレイピングだ!(!?)
作ったもの(実際に稼働したときのツイート)
勝手に拾ってリツイート
今2022/06/16 18:03
— sagamax@サガとレトロゲー (@sagamax__) June 16, 2022
木野田 永志さんの絵#ロマサガRSの絵#ロマサガRShttps://t.co/ahTsGxTHIZ
勝手に自分のサイトに公開
今2022/06/16 18:03
— sagamax@サガとレトロゲー (@sagamax__) June 16, 2022
twitterに公開されたロマサガRSの絵氏さんのツイートを下記リンク先の画面で検索できます。
直前でRTしたツイートも登録済みです。#ロマサガRShttps://t.co/LW0D58Uw9M
勝手にひとつ前の作品を拾ってリツイート
今2022/06/16 18:03
— sagamax@サガとレトロゲー (@sagamax__) June 16, 2022
木野田 永志さんの1つ前の作品はこちら。#ロマサガRShttps://t.co/lcLGaB0lrW
どんな仕組み?
botが勝手に情報収集して勝手にDB登録して勝手に公開してくれる、手間のかからない素敵システム。
from common import accountinfo | |
from datetime import timezone, timedelta | |
import psycopg2 | |
import datetime | |
# SQL | |
INSERT_SQL = "INSERT INTO ALREADY_RETWEET(tweet_id, screen_name, user_name, user_id, tweet_text, url, media_url0, media_url1, media_url2, media_url3, tweet_timestamp) VALUES ('%(tweet_id)s', '%(screen_name)s', '%(user_name)s', '%(user_id)s', '%(tweet_text)s', '%(url)s', '%(media_url0)s', '%(media_url1)s', '%(media_url2)s', '%(media_url3)s', '%(tweet_timestamp)s')" | |
SELECT_SQL = "SELECT COUNT(*) FROM ALREADY_RETWEET WHERE tweet_id = '%(tweet_id)s'" | |
DIFF_JST_FROM_UTC = 9 # UTCとJSTの時差異 | |
DURATION_MINUTES = 10 # 検索範囲(分) | |
SEARCH_COUNT = 10 # 検索件数 | |
def searchKoushikiPic(api, dt_now): | |
# 検索ワード | |
searchWord = "[検索文字列] -filter:retweets" | |
# 検索ワードで検索 | |
resuls = api.search_tweets(q=searchWord, lang='ja', | |
result_type='recent', count=SEARCH_COUNT) | |
for result in resuls: | |
# 対象時間のツイートのみを抽出 | |
# 抽出対象の時間(現在時刻から10分前) | |
dt_now_bfr = dt_now - datetime.timedelta(minutes=DURATION_MINUTES) | |
if dt_now_bfr > result.created_at + datetime.timedelta(hours=DIFF_JST_FROM_UTC): | |
continue | |
postgresInfo = accountinfo.getPostgresInfo() | |
conn = psycopg2.connect( | |
host=postgresInfo["host"], database=postgresInfo["database"], user=postgresInfo["user"], password=postgresInfo["password"]) | |
cur = conn.cursor() | |
count = 0 | |
try: | |
cur.execute(SELECT_SQL % {'tweet_id': result.id_str}) | |
for row in cur: | |
count = row[0] | |
except Exception as e: | |
print(e) | |
finally: | |
cur.close() | |
conn.close() | |
# 過去にRT済みならスキップ | |
if not count == 0: | |
continue | |
if result.text.find("本文に含む文字1") > 0 or result.text.find("本文に含む文字2") > 0: | |
json_obj = result._json | |
# ログ出力 | |
print('tweet_id:' + result.id_str) | |
print('screen_name:' + result.user.screen_name) | |
media_url = ['', '', '', ''] | |
url = '' | |
# 例外処理をする | |
try: | |
if 'entities' in json_obj: | |
if 'media' in json_obj['entities']: | |
if 'url' in json_obj['entities']['media'][0]: | |
url = json_obj['entities']['media'][0]['url'] | |
elif 'urls' in json_obj['entities']: | |
if 'url' in json_obj['entities']['urls'][0]: | |
url = json_obj['entities']['urls'][0]['url'] | |
if 'extended_entities' in json_obj: | |
if 'media' in json_obj['extended_entities']: | |
for i in range(len(json_obj['extended_entities']['media'])): | |
if i < 4: | |
media_url[i] = json_obj['extended_entities']['media'][i]['media_url_https'] | |
except: | |
print('error') | |
print('tweet_id:' + result.id_str) | |
# 例外処理をする | |
try: | |
postgresInfo = accountinfo.getPostgresInfo() | |
conn = psycopg2.connect( | |
host=postgresInfo["host"], database=postgresInfo["database"], user=postgresInfo["user"], password=postgresInfo["password"]) | |
cur2 = conn.cursor() | |
try: | |
cur2.execute(INSERT_SQL % {'tweet_id': result.id_str, 'screen_name': result.user.screen_name, 'user_name': result.user.name, 'user_id': result.user.id_str, 'tweet_text': result.text, 'url': url, | |
'media_url0': media_url[0], 'media_url1': media_url[1], 'media_url2': media_url[2], 'media_url3': media_url[3], 'tweet_timestamp': result.created_at + datetime.timedelta(hours=DIFF_JST_FROM_UTC)}) | |
conn.commit() | |
except Exception as e: | |
print(e) | |
finally: | |
cur2.close() | |
conn.close() | |
# 引用リツイート | |
dateFmt = str(dt_now.strftime('%Y/%m/%d %H:%M')) | |
message = '今' + dateFmt + '\n' | |
message += result.user.name + 'さんの絵\n' | |
message += '#ロマサガRSの絵\n' | |
message += '#ロマサガRS\n' | |
message += 'https://twitter.com/' + \ | |
result.user.screen_name + '/status/' + result.id_str + '\n' | |
api.update_status(message) | |
# 自サイトの画面紹介 | |
tweetSearchGamen(api) | |
# 1つ前の作品をリツイート | |
previousPicture(result, api) | |
# 作品にいいね | |
api.create_favorite(result.id) | |
except: | |
print('error') | |
print('tweet_id:' + result.id_str) | |
def tweetSearchGamen(api): | |
# 画面ツイート | |
dt_now = datetime.datetime.now() | |
dateFmt = str(dt_now.strftime('%Y/%m/%d %H:%M')) | |
message = '今' + dateFmt + '\n' | |
message += '\ntwitterに公開されたロマサガRSの絵氏さんのツイートを下記リンク先の画面で検索できます。\n' | |
message += '直前でRTしたツイートも登録済みです。\n' | |
message += '#ロマサガRS\n' | |
#message += 'https://sagamax.cyou/romasagars/pictures' | |
message += 'https://taumax-github.github.io/sagamax/contents/romasagars/romasaga_pictures.html' | |
api.update_status(message) | |
# 1つ前の作品をリツイート | |
def previousPicture(result, api): | |
SELECT_SQL_PREV_CNT = "SELECT COUNT(*) FROM ALREADY_RETWEET WHERE user_id = '%(user_id)s'" | |
SELECT_SQL_PREV = "SELECT tweet_id, screen_name FROM ALREADY_RETWEET WHERE user_id = '%(user_id)s' ORDER BY tweet_timestamp DESC LIMIT 1 OFFSET 1" | |
# 例外処理をする | |
try: | |
postgresInfo = accountinfo.getPostgresInfo() | |
conn = psycopg2.connect( | |
host=postgresInfo["host"], database=postgresInfo["database"], user=postgresInfo["user"], password=postgresInfo["password"]) | |
cur = conn.cursor() | |
try: | |
cur.execute(SELECT_SQL_PREV_CNT % {'user_id': result.user.id_str}) | |
for row in cur: | |
if row[0] != 0: | |
cur2 = conn.cursor() | |
cur2.execute(SELECT_SQL_PREV % | |
{'user_id': result.user.id_str}) | |
for row2 in cur2: | |
# 引用リツイート | |
dt_now = datetime.datetime.now() | |
dateFmt = str(dt_now.strftime('%Y/%m/%d %H:%M')) | |
message = '今' + dateFmt + '\n' | |
message += result.user.name + 'さんの1つ前の作品はこちら。\n' | |
message += '#ロマサガRS\n' | |
message += 'https://twitter.com/' + \ | |
row2[1] + '/status/' + row2[0] + '\n' | |
api.update_status(message) | |
conn.commit() | |
except Exception as e: | |
print(e) | |
finally: | |
cur.close() | |
cur2.close() | |
conn.close() | |
except: | |
print('error') | |
print('tweet_id:' + result.id_str) | |
def searchArt(account="sagamax_test"): | |
# twitterAPI取得 | |
api = accountinfo.getTweetApi(account, timeoutval=180) | |
# JSTタイムゾーンの生成 | |
JST = timezone(timedelta(hours=+9), 'JST') | |
# 現在時刻 | |
dt_now = datetime.datetime.now(JST) | |
# 公式イラストの収集 | |
searchKoushikiPic(api, dt_now) |
package pictures; | |
import java.io.IOException; | |
import java.sql.Connection; | |
import java.sql.PreparedStatement; | |
import java.sql.ResultSet; | |
import java.sql.SQLException; | |
import java.util.ArrayList; | |
import java.util.List; | |
import javax.servlet.RequestDispatcher; | |
import javax.servlet.ServletException; | |
import javax.servlet.annotation.WebServlet; | |
import javax.servlet.http.HttpServlet; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.servlet.http.HttpServletResponse; | |
import data.DbConnect; | |
/** | |
* Servlet implementation class PicturesServlet | |
*/ | |
@WebServlet(urlPatterns = {"/pictures", "/picsearch"}) | |
public class PicturesServlet extends HttpServlet { | |
private static final long serialVersionUID = 1L; | |
private static final int dispCnt = 5; | |
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { | |
doGet(request, response); | |
} | |
/** | |
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) | |
*/ | |
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { | |
String nowPageNumStr = request.getParameter("nowPageNum"); | |
int nowPageNum = 0; | |
if (nowPageNumStr != null) { | |
int value = Integer.parseInt(nowPageNumStr); | |
nowPageNum = value < 0 ? 0: value; | |
} | |
String userId = request.getParameter("user_id"); | |
String charStyleId = request.getParameter("char_style_id"); | |
userId = (userId == null) ? "%" : userId; | |
String characterId = null; | |
String styleId = null; | |
if (charStyleId == null || charStyleId.equals("%") || charStyleId.equals("")) { | |
characterId = "%"; | |
styleId = "%"; | |
} else { | |
String split[] = charStyleId.split("_"); | |
characterId = split[0]; | |
styleId = split[1]; | |
} | |
// データ取得 | |
List<String> searchResultList = null; | |
int maxCount = 0; | |
try (Connection con = DbConnect.getConnection()){ | |
searchResultList = getPictureInfo(nowPageNum, userId, characterId, styleId, con); | |
maxCount = getMaxCount(userId, characterId, styleId, con); | |
} catch (ClassNotFoundException | SQLException e) { | |
e.printStackTrace(); | |
throw new IllegalStateException(); | |
} | |
request.setAttribute("urlList", searchResultList); | |
request.setAttribute("maxCount", maxCount); | |
request.setAttribute("dispCnt", dispCnt); | |
request.setAttribute("nowPageNum", nowPageNum); | |
int maxPageNum = (int) Math.ceil((double) maxCount / dispCnt); | |
request.setAttribute("maxPageNum", maxPageNum); | |
request.setAttribute("user_id", userId); | |
request.setAttribute("char_style_id", charStyleId); | |
// ページ遷移 | |
RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/jsp/pictures.jsp"); | |
dispatcher.forward(request, response); | |
} | |
/** | |
* 絵師さんのtweetを取得 | |
* @return List<String> tweet urlのリスト | |
*/ | |
private List<String> getPictureInfo(int pageNum, String userId, String characterId, String styleId, Connection con) { | |
String sql = "select screen_name" | |
+ " , tweet_id" | |
+ " from already_retweet" | |
+ " where user_id like ? " | |
+ " and tweet_text like ? " | |
+ " order by tweet_timestamp desc " | |
+ " limit ? offset ? "; | |
String head = "<blockquote class=\"twitter-tweet\"><p lang=\"ja\" dir=\"ltr\"><a href=\"https://twitter.com/"; | |
String tail = "?ref_src=twsrc%5Etfw\"></a></blockquote><script async src=\"https://platform.twitter.com/widgets.js\" charset=\"utf-8\"></script><br>"; | |
List<String> list = new ArrayList<>(); | |
try { | |
PreparedStatement pstmt = con.prepareStatement(sql); | |
pstmt.setString(1, userId); | |
String styleName = null; | |
if (characterId.equals("%")) { | |
styleName = "%"; | |
} else { | |
styleName = getStyleName(characterId, styleId, con); | |
} | |
pstmt.setString(2, "%" + styleName + "%"); | |
pstmt.setInt(3, dispCnt); | |
pstmt.setInt(4, dispCnt * pageNum); | |
ResultSet rs = pstmt.executeQuery(); | |
while (rs.next()) { | |
list.add(head + rs.getString("screen_name") + "/status/" + rs.getString("tweet_id") + tail); | |
} | |
} catch (SQLException e) { | |
e.printStackTrace(); | |
throw new IllegalStateException(); | |
} | |
return list; | |
} | |
/** | |
* 絵師さんのtweetを取得 | |
* @return List<String> tweet urlのリスト | |
*/ | |
private String getStyleName(String characterId, String styleId, Connection con) { | |
String sql = "select style_name" | |
+ " from style" | |
+ " where character_id = ? " | |
+ " and style_id = ? "; | |
String result = null; | |
try { | |
PreparedStatement pstmt = con.prepareStatement(sql); | |
pstmt.setString(1, characterId); | |
pstmt.setString(2, styleId); | |
ResultSet rs = pstmt.executeQuery(); | |
while (rs.next()) { | |
result = rs.getString(1); | |
} | |
} catch (SQLException e) { | |
e.printStackTrace(); | |
throw new IllegalStateException(); | |
} | |
return result; | |
} | |
/** | |
* 絵師さんのtweetを取得 | |
* @return List<String> tweet urlのリスト | |
*/ | |
private int getMaxCount(String userId, String characterId, String styleId, Connection con) { | |
String sql = "select count(*) " | |
+ " from already_retweet" | |
+ " where user_id like ? " | |
+ " and tweet_text like ? "; | |
int maxCount = 0; | |
try { | |
PreparedStatement pstmt = con.prepareStatement(sql); | |
pstmt.setString(1, userId); | |
String styleName = null; | |
if (characterId.equals("%")) { | |
styleName = "%"; | |
} else { | |
styleName = getStyleName(characterId, styleId, con); | |
} | |
pstmt.setString(2, "%" + styleName + "%"); | |
ResultSet rs = pstmt.executeQuery(); | |
while (rs.next()) { | |
maxCount = rs.getInt(1); | |
} | |
} catch (SQLException e) { | |
e.printStackTrace(); | |
throw new IllegalStateException(); | |
} | |
return maxCount; | |
} | |
} |
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> | |
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> | |
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> | |
<%@ taglib prefix="sql" uri="http://java.sun.com/jsp/jstl/sql"%> | |
<%@ page isELIgnored="false" %> | |
<%@ page import = "java.util.ResourceBundle" %> | |
<% | |
ResourceBundle rb = ResourceBundle.getBundle("postgres"); | |
String hostname = rb.getString("[プロパティ名1]"); | |
String portnum = rb.getString("[プロパティ名2]"); | |
String dbname = rb.getString("[プロパティ名3]"); | |
String id = rb.getString("[プロパティ名4]"); | |
String password = rb.getString("[プロパティ名5]"); | |
String url = "jdbc:postgresql://" + hostname + ":" + portnum + "/" + dbname; | |
%> | |
<sql:setDataSource driver="org.postgresql.Driver" | |
var="db" url="<%= url %>" user="<%= id %>" password="<%= password %>" /> | |
<sql:query sql="select y.user_id, user_name | |
from already_retweet x | |
inner join | |
(select user_id, max(tweet_id) as max_tweet_id | |
from already_retweet | |
group by user_id) as y | |
on x.tweet_id = y.max_tweet_id | |
order by user_name " var="rs_screen" dataSource="${db}" ></sql:query> | |
<sql:query sql="select character_id, style_name, min(style_id) as min_style_id from style group by character_id, style_name order by style_name " var="rs_style" dataSource="${db}" ></sql:query> | |
<!DOCTYPE html> | |
<html> | |
<link rel="stylesheet" href="<%= request.getContextPath() %>/WebContent/css/style.css"> | |
<link rel="icon" href="<%= request.getContextPath() %>/WebContent/images/favicon.ico"> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | |
<!-- twitter投稿用の設定 --> | |
<meta name="twitter:card" content="summary" /> <!--カード種類:summary か summary_large_image--> | |
<meta name="twitter:site" content="@sagamax_bot" /> <!--ユーザー名--> | |
<meta property="og:title" content="ロマサガRS公式&お祝いイラスト集" /> <!--記事のタイトル--> | |
<meta property="og:description" content="twitterで公開されたロマサガRSの公式イラストと周年お祝いイラストを検索できる画面です" /> <!--記事の要約(ディスクリプション)--> | |
<meta property="og:image" content="https://taumax-github.github.io/sagamax/images/icon_twitter_card.png" /> <!--画像のURL--> | |
<title>ロマサガRS公式&お祝いイラスト集</title> | |
<style type="text/css"> | |
<!-- | |
* { | |
padding:5px; margin:0px; | |
} | |
body { | |
text-align: center; | |
font-size: 20px; /*文字サイズ*/ | |
color: #fff; /*全体の文字色*/ | |
} | |
br { | |
line-height:1em; | |
} | |
/* データ行 */ | |
table td { | |
text-align: right; | |
color: #fff; /*全体の文字色*/ | |
border:transparent; | |
padding:0px 0px 0px 0px; | |
} | |
/* 奇数行 */ | |
table tr:nth-child(odd) { | |
background-color: transparent; | |
} | |
/* 偶数行 */ | |
table tr:nth-child(even) { | |
background-color: transparent; | |
} | |
.button_min { | |
display : inline-block; | |
border-radius : 5%; /* 角丸 */ | |
font-size : 15pt; /* 文字サイズ */ | |
text-align : center; /* 文字位置 */ | |
cursor : pointer; /* カーソル */ | |
padding : 12px 12px; /* 余白 */ | |
background : #999999; /* 背景色 */ | |
color : #000000; /* 文字色 */ | |
line-height : 0.5em; /* 1行の高さ */ | |
transition : .3s; /* なめらか変化 */ | |
box-shadow : 6px 6px 3px #666666; /* 影の設定 */ | |
border : 2px solid #000000; /* 枠の指定 */ | |
} | |
.button { | |
display : inline-block; | |
border-radius : 5%; /* 角丸 */ | |
font-size : 15pt; /* 文字サイズ */ | |
text-align : center; /* 文字位置 */ | |
cursor : pointer; /* カーソル */ | |
padding : 5px 5px; /* 余白 */ | |
background : #00bfff; /* 背景色 */ | |
color : #000000; /* 文字色 */ | |
line-height : 1.8em; /* 1行の高さ */ | |
transition : .3s; /* なめらか変化 */ | |
box-shadow : 6px 6px 3px #666666; /* 影の設定 */ | |
border : 2px solid #000066; /* 枠の指定 */ | |
} | |
.button_min:hover { | |
box-shadow : none; /* カーソル時の影消去 */ | |
color : #0044CC; /* 文字色 */ | |
background : #ffffff; /* 背景色 */ | |
} | |
.button:hover { | |
box-shadow : none; /* カーソル時の影消去 */ | |
color : #0044CC; /* 文字色 */ | |
background : #ffffff; /* 背景色 */ | |
} | |
.button_min:disabled { | |
box-shadow: none; /* カーソル時の影消去 */ | |
color : #333; /* 文字色 */ | |
background: #777; | |
border : 1px solid #000033; /* 枠の指定 */ | |
} | |
.button_min:disabled:hover { | |
box-shadow: none; /* カーソル時の影消去 */ | |
color : #333; /* 文字色 */ | |
background: #777; | |
border : 1px solid #000033; /* 枠の指定 */ | |
} | |
--> | |
</style> | |
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-156501005-1"></script> | |
<script> | |
window.dataLayer = window.dataLayer || []; | |
function gtag(){dataLayer.push(arguments);} | |
gtag('js', new Date()); | |
gtag('config', 'UA-156501005-1'); | |
</script> | |
<script data-ad-client="ca-pub-5924490903263360" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script> | |
</head> | |
<body class="defaultbody" oncontextmenu="return false;"> | |
<div id="container"> | |
<div id="contents"> | |
<div id="main"> | |
<span id="pagetop"></span> | |
<section class="box"> | |
<!-- tweetボタン --> | |
<div align="center"> | |
<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script><br> | |
</div> | |
<h2 class="title"><a href="https://taumax-github.github.io/sagamax/"> | |
<img src="<%= request.getContextPath() %>/WebContent/images/little_witch_milium_black.png" width="100"></a>ロマサガRSイラスト集</h2> | |
<br> | |
<a href="https://twitter.com/sagamax__" style="color: #00bfff;" target="_blank">@sagamax__</a>がリツイートした絵師さんのツイートを検索できます。<br> | |
<p>追加でリツイートしたものはリアルタイムに当画面に反映されます。<br> | |
pixivは現時点では対象外です。</p> | |
<div align="center"> | |
<form method="post" action="picsearch"> | |
<table> | |
<tr> | |
<td style="text-align:right"> | |
<p>絵師さんの名前:</p> | |
</td> | |
<td style="text-align:left;vertical-align: top;"> | |
<select name="user_id"> | |
<option value="%">未選択</option> | |
<c:forEach var="record" items="${rs_screen.rows}" > | |
<c:choose> | |
<c:when test="${record.user_id == user_id}"> | |
<option value="${record.user_id}" selected>${record.user_name}</option> | |
</c:when> | |
<c:otherwise> | |
<option value="${record.user_id}">${record.user_name}</option> | |
</c:otherwise> | |
</c:choose> | |
</c:forEach> | |
</select> | |
</td> | |
</tr> | |
<tr> | |
<td style="text-align:right"> | |
<p>スタイル名:</p> | |
</td> | |
<td style="text-align:left;vertical-align: top;"> | |
<select name="char_style_id"> | |
<option value="%">未選択</option> | |
<c:forEach var="record" items="${rs_style.rows}" > | |
<c:choose> | |
<c:when test="${record.character_id.concat('_').concat(record.min_style_id) == char_style_id}"> | |
<option value="${record.character_id}_${record.min_style_id}" selected>${record.style_name}</option> | |
</c:when> | |
<c:otherwise> | |
<option value="${record.character_id}_${record.min_style_id}">${record.style_name}</option> | |
</c:otherwise> | |
</c:choose> | |
</c:forEach> | |
</select> | |
</td> | |
</tr> | |
<tr> | |
<td colspan="2" style="text-align:center">指定したスタイル名を本文に含むツイートを表示します。</td> | |
</tr> | |
<tr> | |
<td colspan="2" style="text-align:center"> | |
<input type="submit" value=" 検索 " class="button"> | |
</td> | |
</tr> | |
</table> | |
</form> | |
</div> | |
<div align="right" style="width:65%"> | |
<c:out value="${nowPageNum + 1}/${maxPageNum}頁 全${maxCount}件"/> | |
<table style="width:500px;table-layout: fixed;"> | |
<tr><td width="50" style="text-align:right"> | |
<form method="post" action="picsearch"> | |
<input type="hidden" name="nowPageNum" value=${nowPageNum - 1}> | |
<c:choose> | |
<c:when test="${nowPageNum == 0}"> | |
<input type="submit" value="前へ" class="button_min" disabled> | |
</c:when> | |
<c:otherwise> | |
<input type="submit" value="前へ" class="button_min"> | |
<input type="hidden" name="char_style_id" value="${char_style_id}" /> | |
<input type="hidden" name="user_id" value="${user_id}" /> | |
</c:otherwise> | |
</c:choose> | |
</form> | |
</td> | |
<td width="5" style="text-align:left"> | |
<form method="post" action="picsearch"> | |
<input type="hidden" name="nowPageNum" value=${nowPageNum + 1}> | |
<c:choose> | |
<c:when test="${(nowPageNum + 1) >= maxPageNum}"> | |
<input type="submit" value="次へ" class="button_min" disabled> | |
</c:when> | |
<c:otherwise> | |
<input type="submit" value="次へ" class="button_min"> | |
<input type="hidden" name="char_style_id" value="${char_style_id}" /> | |
<input type="hidden" name="user_id" value="${user_id}" /> | |
</c:otherwise> | |
</c:choose> | |
</form> | |
</td></tr> | |
</table> | |
</div> | |
<div align="center"> | |
<c:forEach items="${urlList}" var="url"> | |
${url} | |
</c:forEach> | |
</div> | |
<div align="right" style="width:65%"> | |
<c:out value="${nowPageNum + 1}/${maxPageNum}頁 全${maxCount}件" /> | |
<table style="width:500px;table-layout: fixed;"> | |
<tr><td width="50" style="text-align:right"> | |
<form method="post" action="picsearch"> | |
<input type="hidden" name="nowPageNum" value=${nowPageNum - 1}> | |
<c:choose> | |
<c:when test="${nowPageNum == 0}"> | |
<input type="submit" value="前へ" class="button_min" disabled> | |
</c:when> | |
<c:otherwise> | |
<input type="submit" value="前へ" class="button_min"> | |
<input type="hidden" name="user_id" value="${user_id}" /> | |
<input type="hidden" name="char_style_id" value="${char_style_id}" /> | |
</c:otherwise> | |
</c:choose> | |
</form> | |
</td> | |
<td width="5" style="text-align:left"> | |
<form method="post" action="picsearch"> | |
<input type="hidden" name="nowPageNum" value=${nowPageNum + 1}> | |
<c:choose> | |
<c:when test="${(nowPageNum + 1) >= maxPageNum}"> | |
<input type="submit" value="次へ" class="button_min" disabled> | |
</c:when> | |
<c:otherwise> | |
<input type="submit" value="次へ" class="button_min"> | |
<input type="hidden" name="user_id" value="${user_id}" /> | |
<input type="hidden" name="char_style_id" value="${char_style_id}" /> | |
</c:otherwise> | |
</c:choose> | |
</form> | |
</td></tr> | |
</table> | |
</div> | |
</section><!-- section class="box" --> | |
</div><!--/#main--> | |
</div><!--/#contents--> | |
</div><!--/#container--> | |
</body> | |
<!--ページの上部に戻る「↑」ボタン--> | |
<p class="nav-fix-pos-pagetop"><a href="#pagetop">↑</a></p> | |
</html> |
ぶつかった壁とか学んだこととか
timezone関係。以下のようなエラーが発生した。
File "/pythonscheduler.py", line 32, in <module>
schedule.run_pending()
File "/usr/local/lib/python3.10/site-packages/schedule/__init__.py", line 780, in run_pending
default_scheduler.run_pending()
File "/usr/local/lib/python3.10/site-packages/schedule/__init__.py", line 100, in run_pending
self._run_job(job)
File "/usr/local/lib/python3.10/site-packages/schedule/__init__.py", line 172, in _run_job
ret = job.run()
File "/usr/local/lib/python3.10/site-packages/schedule/__init__.py", line 661, in run
ret = self.job_func()
File "/artist_rt/SagaRsPictures.py", line 296, in searchArt
searchKoushikiPic(api)
File "/artist_rt/SagaRsPictures.py", line 38, in searchKoushikiPic
if dt_now_bfr > result.created_at.astimezone(datetime.timezone(datetime.timedelta(hours=DIFF_JST_FROM_UTC))):
TypeError: can't compare offset-naive and offset-aware datetimes
can't compare offset-naive and offset-aware datetimes ???何言ってんの???だったんだけど、どうやらpythonのdatetimeオブジェクト(日時=日付と時刻を扱う)とtimeオブジェクト(時刻を扱う)はnaiveとawareの2種類に分類されるらしい。
ざっくり言うと、
- naive:timezone無しのオブジェクト
- aware:timezone付きのオブジェクト
先に乗せたpythonコードの189行目、今は「datetime.datetime.now(JST)」となっているが、以前は「datetime.datetime.now()」となっていたのでnaiveなオブジェクトだった。
同コードの28行目の「if dt_now_bfr > result.created_at + datetime.timedelta(hours=DIFF_JST_FROM_UTC)」でtwitter apiで取得したツイート時刻(UTCが指定されているのでaware)とdatetime.datetime.now()の10分前(native)を比較しようとしているが、pythonの仕様でawareとnativeは比較できないのでエラーになっていた。ということらしい。
# JSTタイムゾーンの生成
JST = timezone(timedelta(hours=+9), 'JST')
# 現在時刻
dt_now = datetime.datetime.now(JST)
「datetime.datetime.now()」の前に「JST = timezone(timedelta(hours=+9), 'JST')」でJSTのtimezoneを生成し、それをnowの引数に渡す(dt_now = datetime.datetime.now(JST))ことでawareなオブジェクトにして対処した。
# 抽出対象の時間(現在時刻から10分前)
dt_now_bfr = dt_now - datetime.timedelta(minutes=DURATION_MINUTES)
if dt_now_bfr > result.created_at + datetime.timedelta(hours=DIFF_JST_FROM_UTC):
continue
参考
- Python, datetime, pytzでタイムゾーンを設定・取得・変換・削除
- datetime --- 基本的な日付型および時間型
- タイムゾーンのインスタンスを生成する
- Pythonのタイムゾーンの扱い
- Python で Datetime を扱う際に気をつけること
- Python: datetimeで確実に日本時間を取得する方法
- 基本的な日付型および時間型