TOP

twitterのスクレイピングbot



ホーム > 個人開発したことまとめ > twitterのスクレイピングbot

twitterのスクレイピングbot

ロマサガRSというソシャゲをやってて、新キャラが実装されるたびに絵師さんがこんなツイートしてたりする。

キャラクタは大量にいるし、いつ誰が公開するかもわからないので自動的に情報収集して検索できるようなもの作れないかな?
 ⇒そうだ!スクレイピングだ!(!?)



作ったもの(実際に稼働したときのツイート)

勝手に拾ってリツイート



勝手に自分のサイトに公開


勝手にひとつ前の作品を拾ってリツイート



どんな仕組み?



botが勝手に情報収集して勝手にDB登録して勝手に公開してくれる、手間のかからない素敵システム。

pythonソースコード(bot)
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)
Java servlet
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;
}
}
view raw gistfile1.txt hosted with ❤ by GitHub
JSP
<%@ 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="&nbsp;&nbsp;&nbsp;&nbsp;検索&nbsp;&nbsp;&nbsp;&nbsp;" 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>
view raw pictures.jsp hosted with ❤ by GitHub


ぶつかった壁とか学んだこととか

timezone関係。以下のようなエラーが発生した。

エラー内容
Traceback (most recent call last):
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オブジェクト(時刻を扱う)はnaiveawareの2種類に分類されるらしい。
ざっくり言うと、

  • naive:timezone無しのオブジェクト
  • aware:timezone付きのオブジェクト

先に乗せたpythonコードの189行目、今は「datetime.datetime.now(JST)」となっているが、以前は「datetime.datetime.now()」となっていたのでnaiveなオブジェクトだった。

# JSTタイムゾーンの生成
JST = timezone(timedelta(hours=+9), 'JST')
# 現在時刻
dt_now = datetime.datetime.now(JST)
同コードの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は比較できないのでエラーになっていた。ということらしい。
# 抽出対象の時間(現在時刻から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
「datetime.datetime.now()」の前に「JST = timezone(timedelta(hours=+9), 'JST')」でJSTのtimezoneを生成し、それをnowの引数に渡す(dt_now = datetime.datetime.now(JST))ことでawareなオブジェクトにして対処した。


参考