RedmineチケットをスクレイピングするPythonスクリプトの作例

RedmineからチケットをスクレイピングするPythonスクリプトの作例です。
一覧画面でページを遷移しながら、一つのCSVファイルに出力します。
手順に沿って解説していきます。

目次

スクレイピング用URLの生成

Redmineのチケット一覧画面で、スクレイピングで使用するURLを生成します。
画面上をクリックしていくだけなので、難しい知識は必要ありません。

EIICHI

当記事はRedmineの公式サイトを例に解説しています。

フィルタとオプションの適用

フィルタとオプションを使って、抽出したい条件や項目を決めます。
最後に左下の「適用」をクリックして、画面に反映させます。

表示件数とページの指定

次に、画面下のナビゲーションを使って、1ページ当たりの表示件数を調整します。
URLの送信回数を減らしたいので、今回は最大の100件に変更しました。
反映されたら、他のページに移動します。
最初のページ以外なら、どのページでも構いません。

表示されたURLの退避

表示されているURLを、メモ帳などに退避します。
スクリプトで使用します。

https://www.redmine.org/projects/redmine/issues?c%5B%5D=tracker&c%5B%5D=status&c%5B%5D=subject&c%5B%5D=assigned_to&c%5B%5D=updated_on&c%5B%5D=category&f%5B%5D=status_id&f%5B%5D=tracker_id&f%5B%5D=&group_by=&op%5Bstatus_id%5D=o&op%5Btracker_id%5D=%3D&page=3&per_page=100&set_filter=1&utf8=%E2%9C%93&v%5Btracker_id%5D%5B%5D=1

Class属性の確認
チケット一覧画面のソースを表示すると、一覧部分のClass属性が「list issues」であることが分かります。この値はCSSセレクタで使用します。

スクリプトの作成

スクリプトの概要

スクリプトの概要は次の通りです。

  • テーブルの取り扱いには「Pandas」ライブラリを使用します。
  • 一覧画面のページを遷移しながら、チケットをファイルに出力します。
  • サーバーへの負荷を考慮して、遷移の間に1秒のスリーブを入れます。
  • ファイルは、1ページ目は上書き、2ページ目以降は追記します。
  • ファイルへの出力は、次のCSV形式とします。
    • ヘッダ行は1ページ目のみ出力します。
    • すべてのフィールドをダブルクォーテーションで囲みます。

「pandas」ライブラリは、次のコマンドでインストールできます。
> pip install pandas lxml html5lib matplotlib

スクリプトと解説

作成したスクリプトは次の通りです。

重要なところをかいつまんで解説します。

チケット一覧画面のURL

# チケット一覧画面のURL
url = ('https://www.redmine.org/projects/redmine/issues'
       '?c%5B%5D=tracker&c%5B%5D=status&c%5B%5D=subject'
       '&c%5B%5D=assigned_to&c%5B%5D=updated_on&c%5B%5D=category'
       '&f%5B%5D=status_id&f%5B%5D=tracker_id'
       '&f%5B%5D=&group_by=&op%5Bstatus_id%5D=o&op%5Btracker_id%5D=%3D'
        # 「page=x」はループ内で正しい値に置き換えます。
       '&page=x&per_page=100'
       '&set_filter=1&utf8=%E2%9C%93&v%5Btracker_id%5D%5B%5D=1')

先ほどメモ帳などに退避したURL、ほぼそのままです。
チケット一覧画面で思い通りに生成したものを、使用するだけなので簡単です。
但し、ページ指定の箇所は、次のreplaceコマンドで正しい値に置き換えるため「page=x」に変更しています。

# 「page=x」を正しい値に置き換えます。
page_url = url.replace('page=x', f'page={i}')

データの抽出

dfs = pd.read_html(page_url, attrs={'class':'list issues'}, index_col=0)

CSSセレクタに、チケット一覧画面で確認したClass属性の「list issues」を指定しています。たったこれだけで、ページのチケットがDataFrameオブジェクトにして返されるなんてホント凄いことです(;゚Д゚)

ファイル出力

if i == 1:
    # 1ページ目:すべて囲む、ファイル上書き、ヘッダ行あり、インデックスなし、改行LF
    dfs[0].to_csv('issues.csv', quoting=csv.QUOTE_ALL, mode='w',
    header=True, index=False, line_terminator='\n')
else:
    # 2ページ目以降:すべて囲む、ファイル追記、ヘッダ行なし、インデックスなし、改行LF
    dfs[0].to_csv('issues.csv', quoting=csv.QUOTE_ALL, mode='a',
    header=False, index=False, line_terminator='\n')

一つのCSVファイルに出力するため、1ページ目と2ページ目以降で、一部引数の値を変えています。下の表にまとめました。

パラメータ1ページ目2ページ目以降
囲み文字quotingcsv.QUOTE_ALL:すべて同左
出力方法mode‘w’:上書き(無ければ作成)‘a’:追記
ヘッダー行headerTrue:出力するFalse:出力しない
インデックスindexFalse:出力しない同左
改行コードline_terminator‘\n’:LF(ラインフィード)同左

実行結果

コンソール

1ページ毎にメッセージを出力しています。

Windows PowerShell

新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6

PS D:\Users\owner\Documents\a1style\vscode> Scraping.py
1ページ目を処理中…
2ページ目を処理中…
3ページ目を処理中…
4ページ目を処理中…
5ページ目を処理中…
6ページ目を処理中…
7ページ目を処理中…
8ページ目を処理中…
9ページ目を処理中…
10ページ目を処理中…
11ページ目を処理中…
12ページ目を処理中…
13ページ目を処理中…
14ページ目を処理中…
PS D:\Users\owner\Documents\a1style\vscode

issues.csv

すべては載せれませんが、参考までに先頭の十数行だけ載せておきます。
改行コードは、指定通り「LF」です。

"#","Tracker","Status","Subject","Assignee","Updated","Category"
"37473","Defect","Confirmed","Focus IssueId when linking issues","","2022-07-20 11:39","Issues"
"37456","Defect","Resolved","undefined local variable or method `twofa_scheme' after a redmine upgrade","","2022-07-18 11:31","Accounts / authentication"
"37453","Defect","New","No email sent to me from redmine.org","","2022-07-19 09:18",""
"37441","Defect","New","No monospace (fixed length) font in markdown any longer","","2022-07-17 06:10","Text formatting"
"37402","Defect","New","ticket topic","","2022-07-12 12:26","Administration"
"37394","Defect","New","uninitialized constant Redmine::WikiFormatting::CommonMark::HTML (NameError)","","2022-07-17 14:39","Text formatting"
"37390","Defect","New","Extraneous whitespace when selecting and copying issue number on Chrome/Windows","","2022-07-07 12:33","UI"
"37388","Defect","Needs feedback","Not Able to Upload Files Using Google Chrome","","2022-07-14 08:13","Attachments"
"37379","Defect","Resolved","Thumbnail macro does not work when a file is attached and preview is displayed immediately","Go MAEDA","2022-07-19 15:57","Text formatting"
"37369","Defect","New","Mention auto-complete not works in bulk-edit comments","Marius BALTEANU","2022-07-04 07:52","Issues"
"37366","Defect","New","Redmine Docker - Easy deploy upgrade, administer","","2022-06-28 13:34","Administration"
"37364","Defect","New","Complex ticket search causes 500 Internal Server Error","","2022-06-29 02:20","Issues filter"
"37358","Defect","New","Trackers can be assigned to users with role that do not have edit option on that tracker","","2022-06-27 13:29","Permissions and roles"
"37282","Defect","Confirmed","subtask isn't displayed correctly since 4.2.7","","2022-07-18 21:43","UI"
(以下、省略)

おわりに

Redmineには、標準でチケットのエクスポート機能があります。
しかしながら、多くの場合、サーバーへの負荷を考慮して、エクスポートできるチケット数の上限を低く設定しているのが実情です。Redmine公式では200件までです。

Pythonによるスクレイピングは、制約を受けずに抽出できるので、自分でデータを分析してみたいユーザーには強力なツールになります。
但し、過度のスクレイピングは、サーバーに負荷をかけ、攻撃とみなされる危険性があります。必ず1ページにつき1秒はスリープを入れましょう。

また、Redmine向けのライブラリ「Python-Redmine」もありますが、こちらはAPIとのことなので、また別の記事でチャレンジできたらと思っています。

最後までご覧いただき、ありがとうございます。
では、また。

コメント

コメントする

目次