Парсим RSS ленту OpenNet.Ru и постим сообщения на форум phpbb средствами Perl

Вопросы и обсуждения, касающиеся программирования на Perl.
Ответить
Аватара пользователя
ZEN
Администратор
Сообщения: 1350
Зарегистрирован: 27 сен 2012, 18:23
Темы: 206
Откуда: Украина, Одесса
Статус: Не в сети

Парсим RSS ленту OpenNet.Ru и постим сообщения на форум phpbb средствами Perl

Сообщение ZEN » 12 сен 2013, 23:19

По мотивам темы парсера RSS новостей на bash. Постепенно переписываю скрипт на Perl. Рабочий прототип ниже, запущу в эксплуатацию в эти выходные.

Код: Выделить всё

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use XML::Parser;
use WWW::Curl::Easy;
## CONFIG
# General config
my $config = "rss.sav";
# PHPBB Forum
my $phpbbnext_login = '';
my $phpbbnext_passw = '';
my $phpbbnext_cookie_file = '/tmp/cookie.txt';
my $phpbbnext_post_login = "username=".$phpbbnext_login."&password=".$phpbbnext_passw."&login=1";
my $phpbbnext_url_login = "http://linuxhub.ru/ucp.php?mode=login";
my $phpbbnext_url_post = "http://linuxhub.ru/posting.php?mode=post&f=7";
my $phpbbnext_url_logout = "http://linuxhub.ru/ucp.php?mode=logout";
my $phpbbnext_text_subject = "Дайджест новостей № ";
my $phpbbnext_text_details = " [Подробнее]";
# RSS
my $rss_url_target = 'http://www.opennet.ru/opennews/opennews_all_noadv.rss';
## END CONFIG

## MAIN LOOP
# Remove old cookie file
unlink($phpbbnext_cookie_file);
# Initial value
my $digest_number = 0;
my $last_update = '';
my $counter = 0;
my $current_tag = '';
my @elements = [];
$elements[$counter] = {};
# Load digest number and last update date from config
load($config);
$digest_number++;
# Download feed
my $xml = get_http_page({
		'url' => $rss_url_target
	}
) or die ("Cannot get RSS feed");
# Parse feed
my $parser = XML::Parser->new(Handlers => {
		Start => \&xml_callback_start,
		End   => \&xml_callback_end,
    	Char  => \&xml_callback_char
	}
);
$parser->parse($xml);
# Buid all feed's in buffer
my $buff = "";
# Flag for skip old news
my $isnew = undef;
# Set in true if last update date unknow
$isnew = 1 if $last_update eq "";
for (my $i = $counter-1; $i >= 0; $i--) {
	if ($isnew) {
		$buff .= "[b]".$elements[$i]->{'title'}."[/b]\n";
		$buff .= $elements[$i]->{'description'};
		$buff .= "[url=".$elements[$i]->{'link'}."]".$phpbbnext_text_details."[/url]";
		$buff .= "\n\n";
	}
	$isnew = 1 if ($elements[$i]->{'pubDate'} eq $last_update);
}
# Exit if news not found
print STDERR "No news found\n" and exit(0) if ($buff eq "");
# Save last update date for config
$last_update = $elements[0]->{'pubDate'};
# Login into a forum
my $tmp = get_http_page({
		'url' 		=> $phpbbnext_url_login, 
		'cookie' 	=> $phpbbnext_cookie_file, 
		'post' 		=> $phpbbnext_post_login
	}
) or print STDERR "I can't get forum login page";		
# FIXME: die ("Cannot login to forum") if $tmp =~ m/Вы успешно вошли в систему/;			
# Get post message page
$tmp = get_http_page({
		'url' 		=> $phpbbnext_url_post, 
		'cookie' 	=> $phpbbnext_cookie_file, 
		'referrer' 	=> $phpbbnext_url_login
	}
) or print STDERR "I can't get forum post page";
# Extract lastclick|creation_time|form_token from post message page
my @val = grep(/lastclick|creation_time|form_token/, split(">", $tmp));
foreach my $key (@val) {
	$key =~ s/\n//g;
	$key =~ s/.*name=\"(.*)\".*value=\"(.*)\".*/$1=$2/g;
}
# Build post message
my $msg = "poll_title=&poll_option_text=&poll_max_options=1&poll_length=0".join("&", @val);
$msg = "message=".$buff."&attach_sig=on&topic_type=0&topic_time_limit=0&post=1&".$msg;
$msg = "subject=".$phpbbnext_text_subject.$digest_number."&addbbcode40=0&addbbcode20=100&".$msg;
# Send post message to forum
$tmp = get_http_page({
		'url' 		=> $phpbbnext_url_post, 
		'cookie' 	=> $phpbbnext_cookie_file, 
		'post' 		=> $msg,
		'referrer' 	=> $phpbbnext_url_post
	}
) or print STDERR "I can't send message to forum";
# Logout from forum
$tmp = get_http_page({
		'url' 		=> $phpbbnext_url_logout, 
		'cookie' 	=> $phpbbnext_cookie_file,
		'referrer' 	=> $phpbbnext_url_post
	}
) or print STDERR "I can' get forum logout page";
# Save digest number and last update date to config
save($config);

exit (0);
## END MAIN LOOP 

## FUNCTION LIST
# Subroute for load data from file
sub load {
	my $file = shift;
	open(FH, "<", "$file") or print STDERR "File $file not found\n" and return undef;
	my $cnt = 0;
	while (<FH>) {
		chomp;
		$digest_number = $_ if $cnt == 0;
		$last_update = $_ if $cnt == 1;
		last if $cnt == 2;
		$cnt++;
	}
	close(FH);
}
# Subroute for save data into file
sub save {
	my $file = shift;
	return undef unless ($file);
	open(my $F, '>', "$file") or die $!;
    binmode($F);
    print $F $digest_number."\n";
	print $F $last_update."\n";
    close($F);
}
# Subroute when openning xml tag 
sub xml_callback_start {
	shift;
	$current_tag = shift;
	if ($current_tag eq "item") {
		$elements[$counter] = {};
	}
}
# Subroute when closing xml tag 
sub xml_callback_end {
	shift;
	my $end_tag = shift;
	if ($end_tag eq "item") {
		$counter++;
	}
	$current_tag = '';
}
# Subroute when extract data between xml tag
sub xml_callback_char {
	shift;
	my $value = shift;
	
	if ($current_tag eq 'link') {
		$elements[$counter]->{'link'} = $value;
	} elsif ($current_tag eq 'guid') {
		$elements[$counter]->{'guid'} = $value;
	} elsif ($current_tag eq 'description') {
		$elements[$counter]->{'description'} .= $value;
	} elsif ($current_tag eq 'title') {
		$elements[$counter]->{'title'} .= $value;
	} elsif ($current_tag eq 'pubDate') {
		$elements[$counter]->{'pubDate'} = $value;
	}
}
# Subroute for getting page via http
sub get_http_page {
	# Return if url param empty
	my %data = %{shift()};
	return undef if (!defined $data{'url'});
    my $curl = WWW::Curl::Easy->new;
    $curl->setopt(CURLOPT_URL, $data{'url'});
	$curl->setopt(CURLOPT_COOKIEFILE, $data{'cookie'}) if (defined $data{'cookie'});
	$curl->setopt(CURLOPT_COOKIEJAR, $data{'cookie'}) if (defined $data{'cookie'});
	$curl->setopt(CURLOPT_POSTFIELDS, $data{'post'}) if (defined $data{'post'});
	$curl->setopt(CURLOPT_REFERER, $data{'referrer'}) if (defined $data{'referrer'});
    # A filehandle, reference to a scalar or reference to a typeglob can be used here.
    my $response_body;
    $curl->setopt(CURLOPT_WRITEDATA,\$response_body);
    # Starts the actual request
    my $retcode = $curl->perform;
    # Looking at the results...
    if ($retcode == 0) {
		return $response_body;
    } 
    return undef;
}
## END OF FUNCTION LIST
бог создал труд и обезьяну
чтоб получился человек
а вот пингвина он не трогал
тот сразу вышел хорошо

Аватара пользователя
ZEN
Администратор
Сообщения: 1350
Зарегистрирован: 27 сен 2012, 18:23
Темы: 206
Откуда: Украина, Одесса
Статус: Не в сети

Re: Парсим RSS ленту OpenNet.Ru и постим сообщения на форум phpbb средствами Perl

Сообщение ZEN » 04 окт 2013, 16:02

На выходных обновлю этот код и запущу на постоянную работу раз в сутки. Новая версия скрипта будет за один раз публиковать все непрочитанные сообщения (Выполнено)
бог создал труд и обезьяну
чтоб получился человек
а вот пингвина он не трогал
тот сразу вышел хорошо

Аватара пользователя
ZEN
Администратор
Сообщения: 1350
Зарегистрирован: 27 сен 2012, 18:23
Темы: 206
Откуда: Украина, Одесса
Статус: Не в сети

Re: Парсим RSS ленту OpenNet.Ru и постим сообщения на форум phpbb средствами Perl

Сообщение ZEN » 18 авг 2015, 14:01

Новая версия скрипта!

Код: Выделить всё

#!/usr/bin/perl -CS

use strict;
use warnings;

use Encode qw(decode);
use Getopt::Long qw(GetOptions);
use POSIX qw(strftime);

{
    package RssToPHPBB;

    use Date::Parse qw(str2time);
    use LWP::UserAgent;
    use HTTP::Cookies;
    use XML::RSS;

    use constant {
        APP_NAME    =>  'RssBB Poster',
        APP_VERS    =>  '1.2',
        APP_HOME    =>  'http://linuxhub.ru',
        INIT_KEYS   =>  [ 'f_url', 'f_login', 'f_pass', 'f_post_id', 'f_post_subj', 'save_file',
                          'f_post_label', 'rss_url', 'debug', 'errors_handler', 'debug_handler' ],
        POST_TRIES  =>  3,
        TIMEOUT     =>  180,
        F_LOGIN_PATH    => 'ucp.php?mode=login',
        F_NEW_TOPIC     => 'posting.php?mode=post&f=',
        MIN_NEWS_COUNT  => 5,
    };

    our @data = ();
    our $cookies = HTTP::Cookies->new();
    our $digest_number = 0;
    our $has_errors    = 0;
    our $last_update   = '';

    sub new {
        my $class = shift;
        my %args = @_;
        my $self = {};

        for my $key ( @{INIT_KEYS()} ) {
            $self->{$key} = $args{$key} if $args{$key};
        }

        $self->{ua} = LWP::UserAgent->new(
            timeout => TIMEOUT,
            agent   => 'Mozilla/5.0 (compatible; ' . APP_NAME . '/' . APP_VERS . '; +' . APP_HOME . ')',
        );
        $self->{ua}->cookie_jar($cookies);

        bless $self, $class;
        return $self;
    }

    sub _debug {
        my ($self, $msg) = @_;
        return unless $self->{debug};

        if ( ref $self->{debug_handler} eq 'CODE' ) {
            $self->{debug_handler}->($msg);
            return;
        }

        print STDERR "[debug] $msg\n" if $self->{debug};
    }

    sub _error {
        my ($self, $msg) = @_;
        $has_errors++;

        if ( ref $self->{errors_handler} eq 'CODE' ) {
            $self->{errors_handler}->($msg);
            return;
        }

        print STDERR "[error] $msg\n";
    }

    sub load {
        my $self = shift;

        $self->_debug( "Load last digest info from file: $self->{save_file}" );
        if ( open( FH, "<", $self->{save_file} ) ) {
            my $cnt = 0;
            while ( <FH> ) {
                chomp;
                $digest_number = $_ if $cnt == 0;
                $last_update   = $_ if $cnt == 1;
                $cnt++;
                last if ( $cnt > 1 );
            }
            close( FH );

            $self->_debug( "Previous digest number: $digest_number" ) if $digest_number;
            $self->_debug( "Previous date of update: $last_update" ) if $last_update;
        }

        $self->_debug( "Load RSS Feed from: $self->{rss_url}" );
        my $response = $self->{ua}->get( $self->{rss_url} );

        unless ( $response->is_success() ) {
            $self->_error( "Cannot load RSS Feed" );
            return $self;
        }

        my $content = $response->content();
        my $rss = XML::RSS->new();
        $rss->parse( $content );
        push( @data, @{$rss->{items}} );

        $self->_debug( "Found " . scalar( @data ) . " item(s) in the RSS Feed" );

        return $self;
    }

    sub save {
        my $self = shift;
        return if $has_errors;

        $self->_debug( "Save digest info to file: $self->{save_file}" );

        if ( open( FH, '>', $self->{save_file} ) ) {
            binmode( FH );
            print FH "$digest_number\n";
            print FH "$last_update\n";
            close( FH );
        } else {
            $self->_error( "Can't save digest info: $!" );
        }
    }

    sub do_work {
        my $self = shift;


        # Build all feeds in post_msg
        my $post_msg = "";

        my $count_of_new_news = 0;
        my $m_last_update;
        for my $item( reverse( @data ) ) {
            if ( !$last_update || ( $last_update && str2time( $item->{pubDate} ) > str2time( $last_update ) ) ) {
                # FIXME: Dirty hack to fix numeric entities after double encoding on RSS server side
                $item->{title} =~ s/&#(\d+);/chr($1)/ge;
                $item->{description} =~ s/&#(\d+);/chr($1)/ge;

                $self->_debug( "New item found: '$item->{title}'" );
                $post_msg .= "[b]" . $item->{title} . "[/b]\n";
                $post_msg .= $item->{'description'} . "\n";
                $post_msg .= "[url=" . $item->{link} . "]" .$self->{f_post_label} . "[/url]";
                $post_msg .= "\n\n\n";
                $count_of_new_news++;
                $m_last_update = $item->{'pubDate'};
            }
        }

        if ( !$count_of_new_news ) {
            $self->_debug( "Sorry, no news" );
            return;
        } elsif ( $count_of_new_news < MIN_NEWS_COUNT() ) {
            $self->_debug( "Too few items: $count_of_new_news of " . MIN_NEWS_COUNT() );
            return;
        }

        # Bump digest number and last update date
        $digest_number++;
        $last_update = $m_last_update;
        $self->_debug( "$count_of_new_news news ready to post" );


        # START Login into a forum
        my $login_url = join( '/', $self->{f_url}, F_LOGIN_PATH() );
        $self->_debug( "Forum login url: $login_url" );
        my $response = $self->{ua}->post( $login_url,
            {
                username => $self->{f_login},
                password => $self->{f_pass},
                redirect => "index.php",
                login    => 1
            }
        );

        unless ( !$response->content() && $response->headers()->{location} ) {
            $self->_error( "Can't login to forum. Status code: " . $response->code() );
            return;
        }
        # END Login into a forum


        # START Get post message page
        my $create_topic_url = join( '/', $self->{f_url}, F_NEW_TOPIC() . $self->{f_post_id} );
        $self->_debug( "Forum create topic url: $create_topic_url" );

        $self->{ua}->default_header( 'Referer' => $create_topic_url );
        $response = $self->{ua}->get( $create_topic_url );

        unless ( $response->is_success() ) {
            $self->_error( "Can't get posting page. Status code: " . $response->code() );
            return;
        }

        $self->_debug( "Get mandatory data from url: $create_topic_url" );

        my $post_form_content = $response->content();
        my ( $creation_time ) = $post_form_content =~ m/.*name=\"(?:creation_time)\" *value=\"(.*)\".*/gm;
        my ( $form_token ) = $post_form_content =~ m/.*name=\"(?:form_token)\" *value=\"(.*)\".*/gm;

        unless ( $creation_time && $form_token ) {
            $self->_error( "Can't get mandatory data to create new topic" );
            return;
        }

        my $post_subj = $self->{f_post_subj};
        $post_subj =~ s/DIGEST_NUM/$digest_number/;

        my ( $is_sended, $number_of_tries ) = ( undef, POST_TRIES() );
        while ( $number_of_tries > 0 && !$is_sended ) {
            # Send post message to forum
            $response = $self->{ua}->post( $create_topic_url,
                {
                    post              => 1,
                    subject           => $post_subj,
                    message           => $post_msg,
                    creation_time     => $creation_time,
                    form_token        => $form_token,
                }
            );

            if ( !$response->content() && $response->headers()->{location} ) {
                $is_sended = 1;
            } else {
                $self->_debug( "Trying to post news on forum" ) if ( $number_of_tries == POST_TRIES() );
                $self->_debug( $number_of_tries-- . " ... Status code: " . $response->code() );
            }
        }

        $self->_error( "News was not posted on forum" ) unless ( $is_sended );
        # END Get post message page
    }
}

my $forum_url;
my $forum_login;
my $forum_passw;
my $forum_post_id;
my $forum_post_subj = decode( 'utf8', 'Дайджест новостей № DIGEST_NUM' );
my $forum_post_label = decode( 'utf8', '[Подробнее]' );
my $save_file = '/tmp/rss.save';
my $rss_url = 'http://www.opennet.ru/opennews/opennews_all_noadv.rss';
my $debug;

sub usage {
    print STDERR "Usage: $0 [ARGS]\n\n";
    print STDERR "Mandatory arguments:\n";
    print STDERR "\t--forum-url='http://linuxhub.ru'\n";
    print STDERR "\t--forum-login='TestUser'\n";
    print STDERR "\t--forum-passw='TestPassword'\n";
    print STDERR "\t--forum-post-id=7\n\n";
    print STDERR "Optional arguments:\n";
    print STDERR "\t--forum-post-subj='$forum_post_subj'\n";
    print STDERR "\t--forum-post-label='$forum_post_label'\n";
    print STDERR "\t--save-file='$save_file'\n";
    print STDERR "\t--rss-url='$rss_url'\n";
    print STDERR "\t--debug\n";
}

sub error_msg {
    my $msg = shift;
    printf STDERR "[%s] [%s] %s\n", strftime("%F %T %z", localtime), "error", $msg;
}

sub debug_msg {
    my $msg = shift;
    return unless ($debug);
    printf STDERR "[%s] [%s] %s\n", strftime("%F %T %z", localtime), "debug", $msg;
}

my $help = 0;

GetOptions(
    "help"      => \$help,
    "debug"     => \$debug,
    "rss-url=s" => \$rss_url,
    "save-file=s"   => \$save_file,
    "forum-url=s"   => \$forum_url,
    "forum-login=s" => \$forum_login,
    "forum-passw=s" => \$forum_passw,
    "forum-post-id=i"    => \$forum_post_id,
    "forum-post-subj=s"  => \$forum_post_subj,
    "forum-post-label=s" => \$forum_post_label,
) or usage() and die( "Error in command line arguments\n" );

$help = 1 unless ($forum_url && $forum_login && $forum_passw && $forum_post_id);
usage() and exit(0) if $help;

my $poster = RssToPHPBB->new(
    f_url          => $forum_url,
    f_login        => $forum_login,
    f_pass         => $forum_passw,
    f_post_id      => $forum_post_id,
    f_post_subj    => $forum_post_subj,
    f_post_label   => $forum_post_label,
    save_file      => $save_file,
    rss_url        => $rss_url,
    debug          => $debug,
    errors_handler => \&error_msg,
    debug_handler  => \&debug_msg,
);

$poster->load();
$poster->do_work();
$poster->save();

exit(0);

Кое-что в скрипте еще нужно доработать, но тем не менее им можно пользоваться уже сейчас. Ждать, когда появится время на доработку, уже не могу - скрипт и так почти месяц провалялся в бардачке :)
бог создал труд и обезьяну
чтоб получился человек
а вот пингвина он не трогал
тот сразу вышел хорошо

Аватара пользователя
ZEN
Администратор
Сообщения: 1350
Зарегистрирован: 27 сен 2012, 18:23
Темы: 206
Откуда: Украина, Одесса
Статус: Не в сети

Re: Парсим RSS ленту OpenNet.Ru и постим сообщения на форум phpbb средствами Perl

Сообщение ZEN » 13 окт 2015, 17:29

Обновил скрипт в прошлом сообщении, так как в RSS попадается текст подобный "&#8212;" и который при декодировании превращается просто в "&#8212;". Следующий фикс был добавлен, что бы преобразовывать подобные сущности из HTML кода в соответствующий символ.

Код: Выделить всё

@@ -142,6 +141,10 @@ use POSIX qw(strftime);
         my $m_last_update;
         for my $item( reverse( @data ) ) {
             if ( !$last_update || ( $last_update && str2time( $item->{pubDate} ) > str2time( $last_update ) ) ) {
+                # FIXME: Dirty hack to fix numeric entities after double encoding on RSS server side
+                $item->{title} =~ s/&#(\d+);/chr($1)/ge;
+                $item->{description} =~ s/&#(\d+);/chr($1)/ge;
+
                 $self->_debug( "New item found: '$item->{title}'" );
                 $post_msg .= "[b]" . $item->{title} . "[/b]\n";
                 $post_msg .= $item->{'description'} . "\n";
бог создал труд и обезьяну
чтоб получился человек
а вот пингвина он не трогал
тот сразу вышел хорошо

Ответить

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 0 гостей