Extraktion von Links aus der Skype-History

Code-Stücke können hier veröffentlicht werden.
Antworten
webspider
User
Beiträge: 485
Registriert: Sonntag 19. Juni 2011, 13:41

Wie ihr vielleicht wisst, ist der Zugriff auf die Skype-History sehr behäbig und scheint sogar Speicherlecks zu verursachen. Daher habe ich ein wenig nachgeforscht und mir ein Skript erstellt, welches für mich sämtliche Links extrahiert. Die entstandene Liste könnte man dann zum Beispiel so weiterverarbeiten:

Code: Alles auswählen

grep -E "png|jpg|gif" skype_links.txt > image_list.txt

Code: Alles auswählen

#! /usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function
import codecs, sqlite3, re

# replace with the path to main.db, the database containing your skype history
HISTORY_FILE = "/Users/username/Library/Application Support/Skype/username/main.db"
OUTPUT_FILE = "skype_links.txt"
SQL_STATEMENT = "SELECT body_xml FROM Messages"
PATTERN = re.compile("<a href=\"(?P<url>.*?)\">(?P<text>.*?)</a>")

def main():
    links = set() # if you use a set instead of a list you don't need to remove duplicates
    connection = sqlite3.connect(HISTORY_FILE)
    c = connection.cursor()
    
    for message in c.execute(SQL_STATEMENT):
        # for some reason simply selecting the message body doesn't suffice
        # so I check whether the first element of it exists at all
        if message[0]:
            # I need to use a loop because I don't know how many URLs the message contains
            for match in re.finditer(PATTERN, message[0]):
                links.add(match.group("url"))

    # this is needed to make sure it works with python 2.x and 3.x
    with codecs.open(OUTPUT_FILE, "w", "utf-8") as output_file:
        output_file.write("\n".join(sorted(links)))
    print("{} links were written".format(len(links)))

if __name__ == "__main__":
    main()
Evtl. baue ich später noch Unterstützung von Kommandozeilenparametern durch argparse ein (Eingabedatei, Ausgabedatei, Ausgabeformat). Sollten andere beschämende Probleme vorliegen, sagt ruhig Bescheid.
BlackJack

@webspider: Du müsstest eigentlich lange genug hier mitlesen um zu wissen, dass `re` und XML zwangsläufig zu einem Hinweis auf XML-Parser führen wird. ;-)

Der Kommentar über dem ``if message[0]`` ist falsch. Damit prüfst Du nicht ob das Element existiert, sondern ob dessen Wert im Boole'schen Kontext wahr ist. Dazu muss es zwingend existieren! Würde es das nicht, bekämst Du an der Stelle für den Test einen `IndexError` um die Ohren gehauen. Ich nehme einfach mal an, dass nicht jede Message ein `body_xml` hat, und da Nullwerte in der Tabelle vorkommen dürfen‽ Dann bekommt man `None` als Wert.

Die Datenbankverbindung wird nicht wieder geschlossen. Und ich hätte `cursor` ausgeschrieben.
webspider
User
Beiträge: 485
Registriert: Sonntag 19. Juni 2011, 13:41

Ich würde ja auch einen XML-Parser einsetzen, aber doch nicht wenn die Nachrichten nicht einmal korrektes XML sind, sondern in der überwiegenden Mehrheit der Fälle lediglich kurzer Text mit einem Anker oder vielleicht noch Smiley drin. Bei solchen Fällen kommt mir der Einsatz von lxml wie mit der Kanone auf Spatzen geschossen vor :D

Die Datenbankresultate sind ein wenig merkwürdig. Wenn die Nachricht leer sein soll, erhalte ich ``(None,)``als Rückgabewert statt einem einfachem None, andernfalls entspricht der erste Wert des Tupels dem Nachrichteninhalt. Dennoch hast du Recht, dass mein Kommentar diesbezüglich verwirrender ist als er hilft. Den Rest korrigiere ich mal schnell.
BlackJack

@webspider: Das ist überhaupt nicht merkwürdig. Du erhälst *immer* Tupel. In der Datenbank steht an der Stelle ja auch ein Wert, nämlich ``NULL``. Der SQL-Wert ``NULL`` wird auf den Python-Wert `None` abgebildet und umgehkert.

Edit: Du könntest die SQL-Abfrage auch einfach um eine ``WHERE``-Klausel erweitern, die ``NULL``-Werte ausfiltert, dann brauchst Du auf der Python-Seite keinen extra Test dafür machen.

Edit2: Mit `lxml` statt `re` (ungetestet):

Code: Alles auswählen

#! /usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function

import codecs
import sqlite3

from lxml import etree

# 
# Replace with the path to main.db, the database containing your skype history.
# 
HISTORY_FILENAME = (
    '/Users/username/Library/Application Support/Skype/username/main.db'
)
OUTPUT_FILENAME = 'skype_links.txt'
SQL_STATEMENT = 'SELECT body_xml FROM Messages WHERE body_xml IS NOT NULL'


def main():
    links = set()
    connection = sqlite3.connect(HISTORY_FILE)
    cursor = connection.cursor()
    for message, in cursor.execute(SQL_STATEMENT):
        links.update(etree.fromstring(message).xpath('//a/@href'))
    connection.close()
    
    with codecs.open(OUTPUT_FILE, 'w', 'utf-8') as output_file:
        output_file.write('\n'.join(sorted(links)))
    print('{0} links were written'.format(len(links)))


if __name__ == "__main__":
    main()
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

Wieso lxml, Python hat auch einen etree in seiner Standard-Bibliothek.
the more they change the more they stay the same
BlackJack

@Dav1d: Das kann aber nix. Zum Beispiel kein XPath.
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

@BlackJack, braucht man auch nicht (zumindest hier), Beispiel aus der Doku:

Code: Alles auswählen

>>> from xml.etree.ElementTree import ElementTree
>>> tree = ElementTree()
>>> tree.parse("index.xhtml")
<Element 'html' at 0xb77e6fac>
>>> p = tree.find("body/p")     # Finds first occurrence of tag p in body
>>> p
<Element 'p' at 0xb77ec26c>
>>> links = list(p.iter("a"))   # Returns list of all links
>>> links
[<Element 'a' at 0xb77ec2ac>, <Element 'a' at 0xb77ec1cc>]
>>> for i in links:             # Iterates through all found links
...     i.attrib["target"] = "blank"
>>> tree.write("output.xhtml")
the more they change the more they stay the same
lunar

@BlackJack: Du musst "lxml.html.fragments_fromstring()" verwenden, und jeden gefundenen Knoten einzeln durchsuchen, da Nachrichten in der Skype-Datenbank kein Root-Element haben, und mithin kein gültiges XML sind und mit ".fromstring()" nicht geparst werden können.

Statt "connection.close()" aufzurufen, kannst Du ein "with"-Block verwenden:

Code: Alles auswählen

with sqlite3.connect(…) as connection:
    …
Mein Versuch:

Code: Alles auswählen

#! /usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function, unicode_literals

import os
import sqlite3
from argparse import ArgumentParser

import lxml.html


DEFAULT_HISTORY = '~/Library/Application Support/Skype/{username}/main.db'
SQL_STATEMENT = 'SELECT body_xml FROM Messages WHERE body_xml IS NOT NULL'


def extract_links_from_message(message):
    fragments = lxml.html.fragments_fromstring(message)
    nodes = (f for f in fragments if not isinstance(f, basestring))
    for node in nodes:
        for link in node.xpath('//a/@href'):
            yield link


def extract_links_from_history(filename):
    with sqlite3.connect(filename) as connection:
        cursor = connection.cursor()
        for message, in cursor.execute(SQL_STATEMENT):
            for link in extract_links_from_message(message):
                yield link


def find_history_file(skype_username):
    return os.path.expanduser(DEFAULT_HISTORY.format(username=skype_username))


def main(argv=None):
    parser = ArgumentParser(description='Extract links from Skype history')
    parser.add_argument('-s', '--sort', help='Sort links alphabetically',
                        action='store_true')
    parser.add_argument('-u', '--unique', help='Print each link only once',
                        action='store_true')
    parser.add_argument('username', help='The Skype username')
    args = parser.parse_args(argv)

    links = extract_links_from_history(find_history_file(args.username))
    if args.unique:
        links = set(links)
    if args.sort:
        links = sorted(links)

    for link in links:
        print(link)


if __name__ == "__main__":
    main()
Antworten