[Tutorial] Download einer Webseite

  • Antworten:19
  • OffenStickyNicht beantwortet
  • Forum-Beiträge: 1.793

04.09.2013 18:31:25 via Website

Hallo zusammen!

Da hier wirklich oft nachgefragt wird, wie man eine Webseite herunterlädt, werde ich hier mal ein (mehr oder weniger:P) kurzes Tutorial dazu posten!

Wer den KOMPLETTEN CODE lesen will, der steht weiter unten. Wer das Tut ließt, lernt aber mehr:wink:

Das Problem:
Das Problem beim Download einer Webseite/beliebigen Datei aus dem Internet ist, dass dieser nicht in wenigen Millisekunden durchzuführen ist. Das liegt daran, dass, bis man die Datei von dem Webserver bekommt, schon mehrere Sekunden vergehen können. In Gebieten mit schlechter Internetverbindung sogar noch länger.
Während wir nun also auf die Antwort des Webservers warten, kann unsere App nichts anderes tun. Das bedeuted, dass der User z.B. keine Knöpfe anklicken kann (z.B. zum Abbrechen), die Anzeige nicht aktualisiert werden kann (z.B. Status des Downloads) und nach 5 Sekunden Android die App automatisch mit einer "Die App reagiert nicht mehr"-Meldung beendet.
Man muss also bewerkstelligen, dass die App zwei Aufgaben gleichzeitig ausführt: Einmal warten auf den Download der Datei, andererseits muss sich die App um die Anzeige für den User kümmern. Um diese parallele Abarbeitung von Aufgaben zu bewerkstelligen, muss man Threads nutzen.
Jeder Thread steht dabei für eine Aufgabe, um die er sich kümmert. Das Betriebssystem (also Android) gibt dem Thread ein paar Millisekunden Zeit den Prozessor zu benutzen und so seine Aufgabe fortzuführen, dann wird der aktuelle Zustand gespeichert und der nächste Thread bekommt das Recht, für ein paar Millisekunden den Prozessor zu nutzen und somit seine Aufgaben auszuführen.
Einen Thread "hat" eine App schon: Den UI-Thread. UI (engl.) steht dabei für "user interface" also "Benutzeroberfläche" und kümmert sich dementsprechend genau darum. Dieser Thread wacht z.B. auf Tippen auf Buttons, zeichnet den Text aus den TextViews auf den Bildschirm usw. usf.
Für den Download müssen wir also einen Download-Thread erstellen, der (z.B. auf Tippen eines Buttons) vom UI-Thread gestartet wird, und ab da automatisch parallel zum UI-Thread ausgeführt wird.

Übrigens, wer den UI-Thread für Netzwerkverbindungen benutzt (also nicht einen neuen Thread erstellt), wird ab Android 3+ mit einer NetworkOnMainThreadException bestraft, die App stürzt also mit der bekannten "Tut uns leid..."-Meldung ab.

Ein wichtiger Hinweis:
Aus bestimmten Gründen dürfen Views (also Button's, TextView's, FrameLayout's usw.) nur von/in dem Thread verändert werden, der sie erstellt hat. Das ist immer der UI-Thread. Der Text eines TextViews zur Ausgabe der Webseite beispielsweise darf nicht von dem Download-Thread durch setText(...) geändert werden. Das darf nur der UI-Thread tun. Haltet ihr euch nicht daran, stürzt eure App mit folgender Fehlermeldung ab: Only the original thread that created a view hierarchy can touch its views.


Wer mehr über Threads lernen möchte oder meine Erklärung nicht verstanden hat, kann z.B. hier weiter lesen.


Die praktische Umsetzung:
Wir müssen als erstes einen neuen Thread erstellen. Am einfachsten ist es dabei, eine neue Klasse zu erstellen, die entweder von java.lang.Thread oder android.os.AsyncTask erbt. AsyncTask ist auf Android zugeschnitten und etwas komfortable zu benutzen.
Daher verwende ich den AsyncTask.
Als erstes erstellt man also eine neue Klasse:
1public class Downloader extends AsyncTask<String, Void, String> {
2}
Damit haben wir das Grundgerüst. Zur Erklärung von <String, Void, String>: Wir nennen hier drei Klassen: Die erste gibt an, mit welchen Objekten der AsyncTask gestartet werden soll. Da wir eine URL übergeben wollen, habe ich String gewählt, die Klasse URL ist natürlich ebenso geeignet.
Die zweite Klasse ist erstmal unwichtig für dieses Tutorial. Unten findet ihr einen kleinen "Exkurs".
Der letzte Parameter gibt an, welches Ergebnis wir am Ende der Aufgabe (bzw. des Threades/AsyncTaskes) zurückliefern wollen: Für den Quelltext einer Webseite eignet sich natürlich String.

Ergänzen wir unserer AsyncTask-Klasse folgende Methode:
1@Override
2protected String doInBackground(String... urls) {
3String response = "";
4for (String url : urls) {
5response += downloadWebPage(url);
6}
7return response;
8}

Wenn wir den AsyncTask starten, erstellt dieser automatisch einen neuen Thread, der die doInBackground-Funktion ausführt.
Der Rückgabewert der Funktion (String) war die dritte Klasse, die wir oben angegeben haben. Der Parameter (String... urls) wurde durch die erste Klasse oben spezifiert. (Für alle, die die drei Punkte nicht kennen.)
Unser Downloader kann also auch mehrere URLs laden und gibt deren Quelltexte dann in einem String hintereinander aus. (Der Sinn sei mal dahingestellt.) Es ist aber natürlich problemlos möglich nur eine URL anzugeben. (s.u.)

Die Schleife geht alle URLs einzeln durch und ruft die Methode downloadWebPage() auf und ergänzt deren Rückgabe zum response. Am Ende wird response zurückgegeben.
Hier könnte man auch einen StringBuilder benutzen, da das Verketten von Strings damit performanter ist. Aus Gründen der Verständlichkeit habe ich dies aber gelassen. Weitere Informationen zur Verwendung z.B. hier oder hier.

Doch nun zur interessanten Methode downloadWebPage(String url):
1private String downloadWebPage(String url) {
2try {
3 HttpClient client = new DefaultHttpClient();
4 HttpGet get = new HttpGet(url);
5 HttpResponse response = client.execute(get);
6 InputStream in = response.getEntity().getContent();
7
8 BufferedReader reader = new BufferedReader(
9 new InputStreamReader(in));
10
11 String source = "";
12 String tmp;
13 while ((tmp = reader.readLine()) != null) {
14 source += tmp;
15 }
16
17 return source;
18 } catch (IOException io) {
19 Log.e("Downloader", "Couldn't downlaod "+url);
20 io.printStackTrace();
21 return "Error when downloading Webpage "+url;
22 }
23}

Zuerst erstellen wir einen client(HttpClient) -vergleichbar mit einem Browser wie IE oder FireFox-, dann eine Anfrage (HttpGet) -vergleichbar mit der Eingabe einer URL in einem Browser. Dann lassen wir den client die Anfrage ausführen und bekommen dafür die Antwort (response) - also den Quelltext der Webseite/das Bild/was auch immer.
Um deren Inhalt zu lesen rufen wir response.getEntity().getContent() auf und bekommen dafür einen InputStream. Da man daraus nur Bytes lesen kann erstellen wir einen InputStreamReader und dann einen BufferedReader. Nun können wir ganz einfach Zeilenweise lesen, bis wir null erhalten, also nichts mehr gelesen werden kann (= Ende der Webseite). Hierbei bietet sich wieder ein StringBuilder an.

Eigentlich könnte unser AsyncTask so schon funktionieren, aber bisher sehen wir noch nicht viel von unserem Ergebnis! Nun kommen wir zum oben genannten Problem: In einem anderen Thread (und damit in der doInBackground-Methode des AsyncTasks) dürfen wir Views nicht verändern, also auch nicht den Text der Webseite z.B. in ein TextView schreiben.
Im AsyncTask kann man aber die onPostExecute()-Methode überschreiben: Diese wird nach der doInBackground von Android wieder auf dem UI-Thread ausgeführt, also dürfen Views in der onPostExecute()-Funktion verändert werden.
Am besten, wir rufen einfach in der onPostExecute wieder unsere Activity auf, die dann das Ergebnis z.B. in einer TextView darstellt.
Dazu verwende ich ein Listener/Interface (genauso wie z.B. ein OnClickListener für einen Button): Die Activity muss, wenn sie ein Objekt vom Downloader erstellt, dann ein Objekt vom Listener/Interface übergeben:
1public static interface DownloadCompleteListener {
2void onDownloadComplete(String result);
3}
4
5private DownloadCompleteListener dc = null;
6public Downloader(DownloadCompleteListener dc) {
7this.dc = dc;
8}
9
10@Override
11protected void onPostExecute(String result) {
12 dc.onDownloadComplete(result);
13}
Der Code besteht aus drei Teilen: Zuerst das Interface, dann der Konstruktor und zuletzt die überschriebene onPostExecute-Methode, welche einfach nur die Methode des Interfaces aufruft, die die Activity überschreiben muss.

Die Aktivity
Hier mal ein Beispiel, wie man unseren tollen Downloader benutzen kann:
1public class MainActivity extends Activity {
2@Override
3 public void onCreate(Bundle arg) {
4super.onCreate(arg);
5 setContentView(R.layout.deinLayout);
6 Button btDownloadStart = (Button) findViewById(R.id.einButton);
7 btDownloadStart.setOnClickListener(new View.OnClickListener() {
8 @Override
9 public void onClick(View b) {
10 startDownload();
11 }
12 });
13 }
14
15}
Der Code dürfte klar sein: Wir laden das Layout, belegen den Button mit dem Listener und rufen wenn jener geklickt wird die startDownload auf.

1private void startDownload() {
2Downloader.DownloadCompleteListener dcl = new Downloader.DownloadCompleteListener() {
3@Override
4public void onDownloadComplete(String result) {
5TextView tv = (TextView) findViewById(R.id.eineTextView);
6tv.setText(result);
7}
8};
9
10Downloader downloader = new Downloader(dcl);
11downloader.execute("Eine URL, die geladen werden soll");
12}
Zuerst erstellen wir ein neues Objekt unseres Interfaces, welches die onDownloadComplete-Methode überschreibt. Dort wird eine TextView gesucht und der Text zugewiesen.
Dann wird ein neues Objekt unseres AsyncTask erstellt und mittels execute() ausgeführt. Wichtig: Würde wir einfach nur die doInBackground()-Funktion ausführen, würde das wie bei jedem Objekt ganz normal auf dem UI-Thread passieren.

Wichtiger Hinweis: Jedes AsyncTask-Objekt kann nur einmal ausgeführt werden!


Weiteres
Jetzt solltet ihr in der Lage sein, eine Webseite herunterzuladen.
Folgendes ist nicht mehr notwendig zu wissen, um dies zu tun, aber für den ein oder anderen vielleicht ganz interessant:
Was ist, wenn man in der doInBackground etwas auf dem UI-Thread ausfphren möchte, z.B. um den aktuellen Fortschritt des Downloads anzuzeigen? Bisher verändern wir Views nur in der onPostExecute()-Funktion, dann ist der Download aber natürlich schon beendet.
Dafür gibt es die nette Funktion publishProgress(Void... progress). Das Void ist die mittlere der drei Klasse von
1class Downloader extends AsyncTask<String, Void, String>
Für einen Fortschritt wäre Integer wahrscheinlich besser.
Rufen wir also publishProgress auf, wird in dem AsyncTask die Funktion onProgressUpdate() auf dem UI-Thread ausgeführt, dass bedeuted, dort darf man Views wieder verändern.
Man kann also onProgressUpdate überschreiben:
1@Override
2 protected void onProgressUpdate(Void... progress) {
3 //Hier Views verändern, um z.B. den Download-Fortschritt anzuzeigen
4 }


Der komplette Code
So nun noch einmal den kompletten Code:
Downloader.java
1import java.io.BufferedReader;
2import java.io.IOException;
3import java.io.InputStream;
4import java.io.InputStreamReader;
5
6import org.apache.http.HttpResponse;
7import org.apache.http.client.HttpClient;
8import org.apache.http.client.methods.HttpGet;
9import org.apache.http.impl.client.DefaultHttpClient;
10
11import android.os.AsyncTask;
12import android.util.Log;
13
14public class Downloader extends AsyncTask<String, Void, String> {
15
16 @Override
17 protected String doInBackground(String... urls) {
18 String response = "";
19 for (String url : urls) {
20 response += downloadWebpage(url);
21 }
22
23 return response.toString();
24 }
25
26 private String downloadWebpage(String url) {
27 try {
28 HttpClient client = new DefaultHttpClient();
29 HttpGet get = new HttpGet(url);
30 HttpResponse response = client.execute(get);
31 InputStream in = response.getEntity().getContent();
32 BufferedReader reader = new BufferedReader(
33 new InputStreamReader(in));
34 String source = "";
35 String tmp;
36 while ((tmp = reader.readLine()) != null) {
37 source += tmp;
38 }
39
40 return source;
41 } catch (IOException io) {
42 Log.e("Downloader", "Couldn't downlaod " + url);
43 io.printStackTrace();
44 return "Error when downloading Webpage" + url;
45 }
46 }
47
48 public interface DownloadCompleteListener {
49 void onDownloadComplete(String result);
50 }
51
52 private DownloadCompleteListener dc = null;
53
54 public Downloader(DownloadCompleteListener dc) {
55 this.dc = dc;
56 }
57
58 @Override
59 protected void onPostExecute(String result) {
60 dc.onDownloadComplete(result);
61 }
62
63}


Und MainActivity.java
1import android.app.Activity;
2import android.os.Bundle;
3import android.view.View;
4import android.widget.Button;
5import android.widget.TextView;
6
7public class MainActivity extends Activity {
8 @Override
9 public void onCreate(Bundle arg) {
10super.onCreate(arg);
11 setContentView(R.layout.einLayout);
12 Button btDownloadStart = (Button) findViewById(R.id.einButton);
13 btDownloadStart.setOnClickListener(new View.OnClickListener() {
14 @Override
15 public void onClick(View b) {
16 startDownload();
17 }
18 });
19 }
20
21 private void startDownload() {
22 Downloader.DownloadCompleteListener dcl = new Downloader.DownloadCompleteListener() {
23 @Override
24 public void onDownloadComplete(String result) {
25 TextView tv = (TextView) findViewById(R.id.eineTextView);
26 tv.setText(result);
27 }
28 };
29
30 Downloader downloader = new Downloader(dcl);
31 downloader.execute("Eine URL, die geladen werden soll");
32 }
33}

So, ich hoffe ihr kommt damit weiter! ;)

— geändert am 01.03.2014 00:29:51

Liebe Grüße impjor.

Für ein gutes Miteinander: Unsere Regeln
Apps für jeden Einsatzzweck
Stellt eure App vor!

  • Forum-Beiträge: 126

04.09.2013 19:51:56 via App

Das ist mir ein wenig zu fortgeschrittenen, kannst du mir vielleicht sagen, wo ich diesen Code einfügen muss

— geändert am 04.09.2013 19:52:23

Mach mir kostenlos eine Freude: http://db.tt/F8VQyVMG Oder auch nicht...

  • Forum-Beiträge: 1.793

04.09.2013 22:13:38 via App

Eigentlich ganz easy: Eine neue Datei erstellen: Downloader.java ,darin der Code der public class Downloader...

Wenn du den Download starten möchtest einfach
1Downloader downloader = new Downloader(ein_TextView_zur_Ausgabe);
2downloader.execute("Deine URL);
VG

***Edit***
Mit geändertem Code funktioniert das nicht mehr! Stattdessen muss statt des TextViews ein Implementation eines Interfaces übergeben werden, so wie z.B. ein OnClickListener der Methode setOnClickListener übergeben werden muss.

— geändert am 10.09.2013 16:39:10

Liebe Grüße impjor.

Für ein gutes Miteinander: Unsere Regeln
Apps für jeden Einsatzzweck
Stellt eure App vor!

  • Forum-Beiträge: 2.545

04.09.2013 23:47:13 via Website

Was passiert, wenn ich den Download-Button drücke, und während der Download dann im Hintergrund läuft, das Gerät um 90 Grad drehe? :ph34r:
  • Forum-Beiträge: 2.545

10.09.2013 18:23:51 via Website

Kann das eine Fang-Frage sein?:grin:

Das will mir nicht gänzlich undenkbar erscheinen :grin:


Leider etwas weniger verständlich, wie ich finde.

Dafür aber besser geeignet für die offenbar verbreitete Seuche "ich kopier das einfach mal, auch wenn ich keine Ahnung habe was es bedeutet" :P

Ich finde es auch nicht weniger lesbar als vorher.
  • Forum-Beiträge: 9

26.02.2014 08:01:30 via Website

Hallo impjor,

ich muss erstmal sagen ich bin beeindruckt von dem Aufwand den Sie damit betrieben haben und möchte mich im Vorfeld schonmal dafür bedanken.

Mein Ziel ist es irgendwann eine App zu schreiben mit der ich bestimmte Informationen aus einer Seite auslesen und weiter verabeiten kann. Als Grundlage dafür habe ich mich an Ihre Anleitung gewagt. Leider zeigt mir Eclipse in der main_activity einige Fehler an die ich nicht verstehe und das auch trotz letzendlichem Kopieren des Codes.
Selbstverständlich habe einen Button und ein TextView eingefügt und die Namen im Code berücksichtigt. Haben Sie vielleichht ein paar Tipps für mich wie man die angezeigten Fehler vermeiden kann oder was man zusätzlich noch an permissions im Manifest bräuchte.
Ich habe inzwischen auch ein wenig rumprobiert und nach meinem eigenen Gutdünken den Code so verändert das die Fehler verschwinden. Allerdings bekomme ich die App im Emulator trotzdem nicht zum Laufen.

Viele Grüße und vielen Dank.
  • Forum-Beiträge: 1.793

27.02.2014 14:30:41 via Website

allXages
Haben Sie vielleichht ein paar Tipps für mich wie man die angezeigten Fehler vermeiden kann oder was man zusätzlich noch an permissions im Manifest bräuchte.
Welche angezeigten Fehler? Könntest du diese etwas genauer beschreiben?

Als Permission im Manifest ist nur INTERNET notwendig.

LG

Liebe Grüße impjor.

Für ein gutes Miteinander: Unsere Regeln
Apps für jeden Einsatzzweck
Stellt eure App vor!

  • Forum-Beiträge: 9

27.02.2014 15:43:20 via Website

Hallo impjor,

vielen Dank das du dich mir an nimmst ;)
Also Internet permission im Manifest hab ich, hab wegen der Fehler mal 2 Screenshots gemacht:


Oben ist der kopierte Code mit den nötigen Ergänzungen und den Fehlern.


Und das hier ist das Ergebnis nach meiner Probiererei, es werden von Eclipse keine Fehler angezeigt, aber beim Starten auf dem Emulator bricht er jedes mal ab mit einer Meldung á la: "leider wurde Outlaw ( so heißt die app) beendet." Könnte das Problem des Absturzes auch noch wo anders liegen?
Ach was mir noch einfällt, in der Downloader.java sagt er noch das "import android.widget.TextView;" nicht verwendet wird und deswegen gelöscht werden kann.

Vielen Dank für die Hilfe

Viele Grüße Philipp
  • Forum-Beiträge: 9

28.02.2014 23:01:10 via Website

Ich habs gefunden, was noch gefehlt hat war die letzte Zeile.

1public class MainActivity extends Activity {
2
3 @Override
4 public void onCreate(Bundle arg) {
5 super.onCreate(arg);
6...

Vielen Dank für den Tipp mit der LogCat, das hat geholfen ;)

Viele Grüße

Philipp
Pascal P.
  • Mod
  • Blogger
  • Forum-Beiträge: 9.207

02.10.2014 15:58:10 via Website

Hallo,
ich habe den Thread mal angepinnt, da er wichtig für Anfänger ist ins sonst noch untergeht.

LG Pascal

LG Pascal //It's not a bug, it's a feature. :) ;)

  • Forum-Beiträge: 20

11.10.2014 17:56:13 via Website

Der Download mit deinem Code hat jetzt geklappt.
Wie aber ist der Code für den Upload?
MfG Gerd

Pascal P.
  • Mod
  • Blogger
  • Forum-Beiträge: 9.207

11.10.2014 18:16:26 via Website

Hallo Gerd,
dieses Tutorial beinhaltet nur den Downlaod von Webseiten, deswegen bitte ich dich dazu im Entwicklerforum einen neuen Thread dazu zu eröffnen.
PS: Google verräht dir dir Lösung auch schnell :P

LG Pascal

LG Pascal //It's not a bug, it's a feature. :) ;)

  • Forum-Beiträge: 4

19.05.2015 17:21:56 via Website

Vielen Dank erstmal für das tolle Tutorial!

Bei mir funktioniert das Downloaden der txt datei und sie wird auch richtig angezeigt... wenn ich jedoch den Text auf dem Server ändere und die App neu starte, wird mir beim drücken des Buttons wieder der alte Text angezeigt, mich würde interressieren wie ich es hinbekomme, dass immer der aktuelle Text angezeigt wird.

Danke im Vorraus,
mfg Thomas

Edit:
Wenn ich das Handy neustarte und die App dann wieder starte, wird der aktuelle Text geladen.. komisch

PS. ich bin Neuling, bitte Newbiegerecht erklären, danke ;)

— geändert am 19.05.2015 17:27:57

Pascal P.
  • Mod
  • Blogger
  • Forum-Beiträge: 9.207

19.05.2015 18:19:05 via App

Dafür ist dein Code wichtig ist dieser 100% aus diesem Tutorial genommen?

LG Pascal //It's not a bug, it's a feature. :) ;)

  • Forum-Beiträge: 4

19.05.2015 19:17:38 via Website

Ja .. ich war so blöd, und hab die Datei nur bei mir aufm PC geändert, und nicht hochgeladen... funktioniert jetzt alles wie gewünscht, danke nochmal! :D

  • Forum-Beiträge: 11

16.02.2016 21:58:08 via Website

Ich habe heute anhand dieses Tutorials asynchrones Speichern von Image-Daten erfolgreich implementiert.

Vielen Dank!

Ludy
  • Mod
  • Blogger
  • Forum-Beiträge: 6.662

08.07.2016 13:14:48 via Website

Hallo zusammen,

ein Hinweis die Klasse Downloader ist seit dem API-Level 22 veraltet.
Es ist nicht viel zuändern.

private String downloadWebpage(String url) {
    try {
        HttpClient client = new DefaultHttpClient();
        HttpGet get = new HttpGet(url);
        HttpResponse response = client.execute(get);
        InputStream in = response.getEntity().getContent();
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(in));
        String source = "";
        String tmp;
        while ((tmp = reader.readLine()) != null) {
            source += tmp;
        }

        return source;
    } catch (IOException io) {
        Log.e("Downloader", "Couldn't downlaod " + url);
        io.printStackTrace();
        return "Error when downloading Webpage" + url;
    }
}

wird zu

private String downloadWebpage(String url) {
    try {
        /** Neues Anfang **/
        URL _url = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) _url.openConnection();
        connection.connect();
        InputStream in = connection.getInputStream();
        /** Neues Ende **/

        BufferedReader reader = new BufferedReader(
                new InputStreamReader(in));
        String source = "";
        String tmp;
        while ((tmp = reader.readLine()) != null) {
            source += tmp;
        }

        return source;
    } catch (IOException io) {
        Log.e("Downloader", "Couldn't downlaod " + url);
        io.printStackTrace();
        return "Error when downloading Webpage" + url;
    }
}

imports die entfernt werden:

  • import org.apache.http.HttpResponse;
  • import org.apache.http.client.HttpClient;
  • import org.apache.http.client.methods.HttpGet;
  • import org.apache.http.impl.client.DefaultHttpClient;

neue imports:

  • import java.net.HttpURLConnection;
  • import java.net.URL;

Gruß Ludy (App Entwickler)

Mein Beitrag hat dir geholfen? Lass doch ein "Danke" da.☺

Lebensmittelwarnung App-Thread

Download Samsung Firmware Tool